diff --git a/checkin-server.js b/checkin-server.js index 2a4b9d4..e1789d4 100644 --- a/checkin-server.js +++ b/checkin-server.js @@ -3,7 +3,7 @@ const express = require('express'); const path = require('path'); const { db } = require('./database'); -const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); +const { getCurrentDate, getCurrentTime, updateTotalHours, formatHoursMin } = require('./helpers/utils'); const checkinApp = express(); 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 }); } 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, { title: successTitle, message: successMessage, diff --git a/helpers/utils.js b/helpers/utils.js index 62ea7f0..e810c17 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -132,6 +132,22 @@ function getWeekStart(dateStr) { 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 = { hasRole, getDefaultRole, @@ -143,5 +159,6 @@ module.exports = { formatDateTime, getCalendarWeek, getWeekDatesFromCalendarWeek, - getWeekStart + getWeekStart, + formatHoursMin }; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 496987d..3f3eb4b 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -51,7 +51,7 @@ async function loadUserStats() { const currentOvertimeEl = document.getElementById('currentOvertime'); if (currentOvertimeEl) { 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'; // Auch die Border-Farbe des Cards anpassen const overtimeCard = currentOvertimeEl.closest('.stat-card'); @@ -548,27 +548,27 @@ function renderWeek() { // Stunden-Anzeige für halben Tag Urlaub berechnen let hoursDisplay = ''; if (isFullDayVacation) { - hoursDisplay = fullDayHours.toFixed(2) + ' h (Urlaub)'; + hoursDisplay = formatHoursMin(fullDayHours) + ' (Urlaub)'; } else if (isHalfDayVacation) { const halfHours = fullDayHours / 2; const workHours = hours || 0; // Das sind die gearbeiteten Stunden (ohne Urlaub) const totalHours = halfHours + workHours; 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 { - hoursDisplay = halfHours.toFixed(2) + ' h (Urlaub)'; + hoursDisplay = formatHoursMin(halfHours) + ' (Urlaub)'; } } else if (isSick) { - hoursDisplay = fullDayHours.toFixed(2) + ' h (Krank)'; + hoursDisplay = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isHoliday && isWeekend) { // Feiertag am Wochenende: keine Tagesarbeitsstunden - hoursDisplay = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; + hoursDisplay = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { - hoursDisplay = fullDayHours.toFixed(2) + ' h (Feiertag)'; + hoursDisplay = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { - hoursDisplay = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; + hoursDisplay = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { - hoursDisplay = hours.toFixed(2) + ' h'; + hoursDisplay = formatHoursMin(hours); } const requiredBreak = (startTime && endTime) ? calculateRequiredBreakMinutes(startTime, endTime) : null; @@ -741,7 +741,7 @@ function renderWeek() { `; 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) @@ -1017,14 +1017,14 @@ function updateOvertimeDisplay() { if (overtimeSummaryItem && overtimeHoursSpan) { overtimeSummaryItem.style.display = 'block'; const sign = overtimeHours >= 0 ? '+' : ''; - overtimeHoursSpan.textContent = `${sign}${overtimeHours.toFixed(2)} h`; + overtimeHoursSpan.textContent = (sign === '+' ? '+' : '') + formatHoursMin(overtimeHours); overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c'; } // Gesamtstunden-Anzeige aktualisieren const totalHoursElement = document.getElementById('totalHours'); if (totalHoursElement) { - totalHoursElement.textContent = totalHoursWithVacation.toFixed(2) + ' h'; + totalHoursElement.textContent = formatHoursMin(totalHoursWithVacation); } } @@ -1272,7 +1272,7 @@ async function saveEntry(input) { if (hoursElement) { if (isFullDayVacation) { // 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; } else if (isHalfDayVacation) { // Halber Tag Urlaub: Berechne Stunden aus Start/Ende falls vorhanden @@ -1293,9 +1293,9 @@ async function saveEntry(input) { const totalHours = halfHours + workHours; 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 { - hoursElement.textContent = halfHours.toFixed(2) + ' h (Urlaub)'; + hoursElement.textContent = formatHoursMin(halfHours) + ' (Urlaub)'; } currentEntries[date].total_hours = totalHours; } else { @@ -1303,15 +1303,15 @@ async function saveEntry(input) { const d = new Date(date); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isSick) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isWeekendHoliday) { - hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; + hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { - hoursElement.textContent = hours.toFixed(2) + ' h'; + hoursElement.textContent = formatHoursMin(hours); } } } @@ -1446,10 +1446,10 @@ async function saveEntry(input) { const isHalfDayVacation = vacationType === 'half'; const fullDayHours = getFullDayHours(); - let hoursText = result.total_hours.toFixed(2) + ' h'; + let hoursText = formatHoursMin(result.total_hours); if (isFullDayVacation) { - hoursText = fullDayHours.toFixed(2) + ' h (Urlaub)'; + hoursText = formatHoursMin(fullDayHours) + ' (Urlaub)'; } else if (isHalfDayVacation) { // Bei halbem Tag Urlaub: result.total_hours enthält nur die gearbeiteten Stunden // Die Urlaubsstunden müssen addiert werden @@ -1458,25 +1458,25 @@ async function saveEntry(input) { const totalHours = halfHours + workHours; // Gesamt = Urlaub + gearbeitet 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 { - hoursText = halfHours.toFixed(2) + ' h (Urlaub)'; + hoursText = formatHoursMin(halfHours) + ' (Urlaub)'; } // Aktualisiere currentEntries mit den Gesamtstunden currentEntries[date].total_hours = totalHours; } else if (isSick) { - hoursText = fullDayHours.toFixed(2) + ' h (Krank)'; + hoursText = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isHoliday) { const d = new Date(date); const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0); if (isWeekendHoliday) { - hoursText = (result.total_hours || 0).toFixed(2) + ' h (Feiertag)'; + hoursText = formatHoursMin(result.total_hours || 0) + ' (Feiertag)'; } else if (result.total_hours <= fullDayHours) { - hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)'; + hoursText = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else { 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 => { 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) updateOvertimeDisplay(); @@ -1591,7 +1591,7 @@ function checkWeekComplete() { if (overtimeValue > fullDayHours) { if (!startTime || !endTime || startTime === '' || endTime === '') { 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 } } @@ -2077,22 +2077,22 @@ function toggleSickStatus(dateStr) { if (newStatus) { // Krank: Zeige fullDayHours mit "(Krank)" Label - hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Krank)'; currentEntries[dateStr].total_hours = fullDayHours; } else { // Zurück zu normaler Anzeige basierend auf anderen Status const d = new Date(dateStr); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isFullDayVacation) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Urlaub)'; } else if (isWeekendHoliday) { - hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; + hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { - hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; + hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { - hoursElement.textContent = hours.toFixed(2) + ' h'; + hoursElement.textContent = formatHoursMin(hours); } } } diff --git a/public/js/format-hours.js b/public/js/format-hours.js new file mode 100644 index 0000000..379c4b0 --- /dev/null +++ b/public/js/format-hours.js @@ -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; +})(); diff --git a/services/pdf-service.js b/services/pdf-service.js index ca12c6e..de25956 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -3,7 +3,7 @@ const PDFDocument = require('pdfkit'); const QRCode = require('qrcode'); const { db } = require('../database'); -const { formatDate, formatDateTime } = require('../helpers/utils'); +const { formatDate, formatDateTime, formatHoursMin } = require('../helpers/utils'); const { getHolidaysWithNamesForDateRange } = require('./feiertage-service'); // Kalenderwoche berechnen @@ -190,7 +190,7 @@ function generatePDF(timesheetId, req, res) { // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen const holidayDay = new Date(row.date + 'T12:00:00').getDay(); 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]; rowData.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); @@ -215,7 +215,7 @@ function generatePDF(timesheetId, req, res) { entry.start_time || '-', entry.end_time || '-', 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) => { @@ -250,7 +250,7 @@ function generatePDF(timesheetId, req, res) { if (activity.projectNumber) { activityText += ` (Projekt: ${activity.projectNumber})`; } - activityText += ` - ${activity.hours.toFixed(2)} h`; + activityText += ` - ${formatHoursMin(activity.hours)}`; doc.fontSize(9).font('Helvetica'); doc.text(activityText, 70, doc.y, { width: 360 }); doc.moveDown(0.2); @@ -264,7 +264,7 @@ function generatePDF(timesheetId, req, res) { overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); } 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) { const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; @@ -308,7 +308,7 @@ function generatePDF(timesheetId, req, res) { doc.font('Helvetica-Bold'); // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden (8h pro Feiertag) 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 const wochenstunden = timesheet.wochenstunden || 0; @@ -318,11 +318,11 @@ function generatePDF(timesheetId, req, res) { doc.moveDown(0.3); doc.font('Helvetica-Bold'); 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) { - doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); + doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y); } else { - doc.text(`Überstunden: 0.00 h`, 50, doc.y); + doc.text(`Überstunden: ${formatHoursMin(0)}`, 50, doc.y); } doc.end(); @@ -462,7 +462,7 @@ function generatePDFToBuffer(timesheetId, req) { // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen const holidayDay = new Date(row.date + 'T12:00:00').getDay(); 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]; rowDataBuf.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); @@ -487,7 +487,7 @@ function generatePDFToBuffer(timesheetId, req) { entry.start_time || '-', entry.end_time || '-', 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) => { @@ -519,7 +519,7 @@ function generatePDFToBuffer(timesheetId, req) { if (activity.projectNumber) { activityText += ` (Projekt: ${activity.projectNumber})`; } - activityText += ` - ${activity.hours.toFixed(2)} h`; + activityText += ` - ${formatHoursMin(activity.hours)}`; doc.fontSize(9).font('Helvetica'); doc.text(activityText, 70, doc.y, { width: 360 }); doc.moveDown(0.2); @@ -532,7 +532,7 @@ function generatePDFToBuffer(timesheetId, req) { overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); } 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) { const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; @@ -573,7 +573,7 @@ function generatePDFToBuffer(timesheetId, req) { doc.moveDown(0.5); doc.font('Helvetica-Bold'); 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 overtimeHours = totalHoursWithVacation - wochenstunden; @@ -581,11 +581,11 @@ function generatePDFToBuffer(timesheetId, req) { doc.moveDown(0.3); doc.font('Helvetica-Bold'); 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) { - doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); + doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y); } else { - doc.text(`Überstunden: 0.00 h`, 50, doc.y); + doc.text(`Überstunden: ${formatHoursMin(0)}`, 50, doc.y); } doc.end(); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 3bc30ce..a2d51d0 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -52,11 +52,11 @@