diff --git a/checkin-server.js b/checkin-server.js index e1789d4..e9c387a 100644 --- a/checkin-server.js +++ b/checkin-server.js @@ -47,7 +47,7 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { const currentTime = getCurrentTime(); // Prüfe ob User existiert - db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT id, default_break_minutes FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 }); } @@ -61,10 +61,14 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { const successTitle = 'Hallo, du wurdest erfolgreich eingecheckt'; + const userDefaultBreakMinutes = Number.isInteger(user?.default_break_minutes) && user.default_break_minutes >= 0 + ? user.default_break_minutes + : 30; + if (!entry) { // Kein Eintrag existiert → Erstelle neuen mit start_time - db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, - [userId, currentDate, currentTime], (err) => { + db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + [userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => { if (err) { return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 }); } diff --git a/public/css/style.css b/public/css/style.css index 5c7f43f..b21245a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -840,6 +840,17 @@ table input[type="text"] { color: #7f8c8d; } +/* Überstunden-Farbklassen (global genutzt, z. B. Verwaltung & Auswertung) */ +.overtime-positive { + color: #27ae60; + font-weight: 600; +} + +.overtime-negative { + color: #e74c3c; + font-weight: 600; +} + /* Activities/Tätigkeiten */ .activities-row { background-color: #f8f9fa; @@ -864,7 +875,7 @@ table input[type="text"] { .activity-row { display: grid; - grid-template-columns: 1fr 150px 120px; + grid-template-columns: 1fr 150px 150px; gap: 15px; align-items: center; } @@ -893,7 +904,7 @@ table input[type="text"] { } .activity-hours-input { - width: 80px; + width: 64px; } .activity-hours-label { @@ -901,6 +912,11 @@ table input[type="text"] { color: #555; } +.activity-hours-hh-input, +.activity-hours-mm-input { + text-align: center; +} + .activity-project { display: flex; align-items: center; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 0472980..b6bb9cf 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -374,6 +374,52 @@ function getFullDayHours() { return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8; } +function decimalHoursToParts(decimalHours) { + const parsed = Number(decimalHours); + if (!Number.isFinite(parsed) || parsed <= 0) { + return { hh: '', mm: '' }; + } + + const totalMinutes = Math.round(parsed * 60); + const hh = Math.floor(totalMinutes / 60); + const mm = totalMinutes % 60; + return { + hh: String(hh), + mm: String(mm).padStart(2, '0') + }; +} + +function parseActivityHoursFromInputs(hoursInput, minutesInput, fallbackValue) { + const hoursRaw = hoursInput ? hoursInput.value.trim() : ''; + const minutesRaw = minutesInput ? minutesInput.value.trim() : ''; + + if (hoursRaw === '' && minutesRaw === '') { + return 0; + } + + const parsedHours = parseInt(hoursRaw, 10); + const parsedMinutes = parseInt(minutesRaw, 10); + + const safeHours = Number.isFinite(parsedHours) && parsedHours >= 0 ? parsedHours : 0; + const safeMinutes = Number.isFinite(parsedMinutes) && parsedMinutes >= 0 ? parsedMinutes : 0; + const totalMinutes = (safeHours * 60) + safeMinutes; + + if (!Number.isFinite(totalMinutes) || totalMinutes < 0) { + return Number.isFinite(fallbackValue) ? fallbackValue : 0; + } + + const normalizedHours = Math.floor(totalMinutes / 60); + const normalizedMinutes = totalMinutes % 60; + if (hoursInput) { + hoursInput.value = totalMinutes > 0 ? String(normalizedHours) : ''; + } + if (minutesInput) { + minutesInput.value = totalMinutes > 0 ? String(normalizedMinutes).padStart(2, '0') : ''; + } + + return totalMinutes / 60; +} + // Woche laden async function loadWeek() { try { @@ -603,7 +649,9 @@ function renderWeek() {
Tätigkeiten:
- ${activities.map((activity, idx) => ` + ${activities.map((activity, idx) => { + const timeParts = decimalHoursToParts(activity.hours); + return `
- + class="activity-hours-input activity-hours-hh-input"> h + + min
- `).join('')} + `; + }).join('')}
@@ -1038,7 +1100,8 @@ function handleOvertimeChange(dateStr, overtimeHours) { // (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit) for (let i = 1; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`); if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') { descInput.value = ''; @@ -1047,6 +1110,10 @@ function handleOvertimeChange(dateStr, overtimeHours) { hoursInput.value = ''; saveEntry(hoursInput); } + if (minutesInput) { + minutesInput.value = ''; + saveEntry(minutesInput); + } } } @@ -1155,7 +1222,18 @@ async function saveEntry(input) { // Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein const start_time = actualStartTime; const end_time = actualEndTime; - let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0); + let break_minutes = defaultBreakMinutes; + if (breakInput && breakInput.value !== '') { + const parsedBreak = parseInt(breakInput.value, 10); + break_minutes = Number.isFinite(parsedBreak) && parsedBreak >= 0 ? parsedBreak : defaultBreakMinutes; + } else if ( + currentEntries[date].break_minutes !== null && + currentEntries[date].break_minutes !== undefined && + currentEntries[date].break_minutes !== '' + ) { + const parsedStoredBreak = parseInt(currentEntries[date].break_minutes, 10); + break_minutes = Number.isFinite(parsedStoredBreak) && parsedStoredBreak >= 0 ? parsedStoredBreak : defaultBreakMinutes; + } const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || ''); const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null); @@ -1168,12 +1246,16 @@ async function saveEntry(input) { const activities = []; for (let i = 1; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`); const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`); + const fallbackHours = parseFloat(currentEntries[date][`activity${i}_hours`]) || 0; activities.push({ desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null), - hours: hoursInput ? (parseFloat(hoursInput.value) || 0) : (parseFloat(currentEntries[date][`activity${i}_hours`]) || 0), + hours: (hoursInput || minutesInput) + ? parseActivityHoursFromInputs(hoursInput, minutesInput, fallbackHours) + : fallbackHours, projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null) }); } @@ -1337,21 +1419,27 @@ async function saveEntry(input) { // 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere if (isFullDayVacation) { const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours_mm"]`); if (descInput) { descInput.value = 'Urlaub'; currentEntries[date].activity1_desc = 'Urlaub'; } if (hoursInput) { - hoursInput.value = fullDayHours.toFixed(2); + const fullDayParts = decimalHoursToParts(fullDayHours); + hoursInput.value = fullDayParts.hh; + if (minutesInput) { + minutesInput.value = fullDayParts.mm; + } currentEntries[date].activity1_hours = fullDayHours; } // Leere andere Tätigkeiten for (let i = 2; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`); const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`); if (descInput) { @@ -1360,6 +1448,9 @@ async function saveEntry(input) { } if (hoursInput) { hoursInput.value = ''; + if (minutesInput) { + minutesInput.value = ''; + } currentEntries[date][`activity${i}_hours`] = 0; } if (projectInput) { @@ -1371,7 +1462,8 @@ async function saveEntry(input) { // Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren for (let i = 1; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`); const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`); if (descInput) { @@ -1380,6 +1472,9 @@ async function saveEntry(input) { } if (hoursInput) { hoursInput.value = ''; + if (minutesInput) { + minutesInput.value = ''; + } currentEntries[date][`activity${i}_hours`] = 0; } if (projectInput) { @@ -2118,7 +2213,8 @@ function toggleSickStatus(dateStr) { // Leere alle Tätigkeitsfelder for (let i = 1; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`); const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`); if (descInput) { @@ -2127,6 +2223,9 @@ function toggleSickStatus(dateStr) { } if (hoursInput) { hoursInput.value = ''; + if (minutesInput) { + minutesInput.value = ''; + } currentEntries[dateStr][`activity${i}_hours`] = 0; } if (projectInput) { @@ -2137,7 +2236,8 @@ function toggleSickStatus(dateStr) { } else { // Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours_mm"]`); const fullDayHours = getFullDayHours(); if (descInput) { @@ -2145,14 +2245,19 @@ function toggleSickStatus(dateStr) { currentEntries[dateStr].activity1_desc = 'Krank'; } if (hoursInput) { - hoursInput.value = fullDayHours.toFixed(2); + const fullDayParts = decimalHoursToParts(fullDayHours); + hoursInput.value = fullDayParts.hh; + if (minutesInput) { + minutesInput.value = fullDayParts.mm; + } currentEntries[dateStr].activity1_hours = fullDayHours; } // Leere andere Tätigkeiten for (let i = 2; i <= 5; i++) { const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`); + const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`); const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`); if (descInput) { @@ -2161,6 +2266,9 @@ function toggleSickStatus(dateStr) { } if (hoursInput) { hoursInput.value = ''; + if (minutesInput) { + minutesInput.value = ''; + } currentEntries[dateStr][`activity${i}_hours`] = 0; } if (projectInput) { diff --git a/routes/timesheet-routes.js b/routes/timesheet-routes.js index 5b7125d..bee7172 100644 --- a/routes/timesheet-routes.js +++ b/routes/timesheet-routes.js @@ -20,6 +20,11 @@ function registerTimesheetRoutes(app) { overtime_taken_hours, vacation_type, sick_status, weekend_travel } = req.body; const userId = req.session.userId; + const hasExplicitBreakMinutes = break_minutes !== undefined && break_minutes !== null && break_minutes !== ''; + const parsedRequestedBreakMinutes = hasExplicitBreakMinutes ? parseInt(break_minutes, 10) : null; + const requestedBreakMinutes = Number.isFinite(parsedRequestedBreakMinutes) && parsedRequestedBreakMinutes >= 0 + ? parsedRequestedBreakMinutes + : null; // Normalisiere end_time: Leere Strings werden zu null const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null); @@ -55,7 +60,7 @@ function registerTimesheetRoutes(app) { } // User-Daten laden (für Überstunden-Berechnung) - db.get('SELECT wochenstunden, arbeitstage FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, arbeitstage, default_break_minutes FROM users WHERE id = ?', [userId], (err, user) => { if (err) { console.error('Fehler beim Laden der User-Daten:', err); return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' }); @@ -63,6 +68,10 @@ function registerTimesheetRoutes(app) { const wochenstunden = user?.wochenstunden || 0; const arbeitstage = user?.arbeitstage || 5; + const defaultBreakMinutes = Number.isInteger(user?.default_break_minutes) && user.default_break_minutes >= 0 + ? user.default_break_minutes + : 30; + let effectiveBreakMinutes = requestedBreakMinutes !== null ? requestedBreakMinutes : defaultBreakMinutes; const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0; const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 0; @@ -106,7 +115,7 @@ function registerTimesheetRoutes(app) { const start = new Date(`2000-01-01T${normalizedStartTime}`); const end = new Date(`2000-01-01T${normalizedEndTime}`); const diffMs = end - start; - total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60); + total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60); // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') { @@ -124,9 +133,26 @@ function registerTimesheetRoutes(app) { // Sie werden über overtime_taken_hours in der PDF angezeigt // Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren - db.get('SELECT id, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', + db.get('SELECT id, break_minutes, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, date], (err, row) => { if (row) { + if (requestedBreakMinutes === null && row.break_minutes !== null && row.break_minutes !== undefined) { + effectiveBreakMinutes = row.break_minutes; + } + + if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) { + const start = new Date(`2000-01-01T${normalizedStartTime}`); + const end = new Date(`2000-01-01T${normalizedEndTime}`); + const diffMs = end - start; + total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60); + if (isWeekend && isWeekendTravel && total_hours > 0) { + const weekendPercentage = getWeekendPercentage(date); + if (weekendPercentage >= 100) { + total_hours = total_hours * (weekendPercentage / 100); + } + } + } + // Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert) let finalAppliedPercentage = appliedWeekendPercentage; if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) { @@ -138,7 +164,7 @@ function registerTimesheetRoutes(app) { const start = new Date(`2000-01-01T${normalizedStartTime}`); const end = new Date(`2000-01-01T${normalizedEndTime}`); const diffMs = end - start; - const baseHours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60); + const baseHours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60); if (baseHours > 0 && finalAppliedPercentage >= 100) { total_hours = baseHours * (finalAppliedPercentage / 100); } @@ -165,7 +191,7 @@ function registerTimesheetRoutes(app) { updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [ - finalStartTime, finalEndTime, break_minutes, total_hours, notes, + finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes, finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, @@ -197,7 +223,7 @@ function registerTimesheetRoutes(app) { overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ - userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes, + userId, date, finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes, finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, diff --git a/services/ping-service.js b/services/ping-service.js index 90f10b6..5761440 100644 --- a/services/ping-service.js +++ b/services/ping-service.js @@ -5,7 +5,7 @@ const { db } = require('../database'); const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils'); // Ping-Funktion für einen User -async function pingUserIP(userId, ip, currentDate, currentTime) { +async function pingUserIP(userId, ip, defaultBreakMinutes, currentDate, currentTime) { try { const result = await ping.promise.probe(ip, { timeout: 3, @@ -31,6 +31,10 @@ async function pingUserIP(userId, ip, currentDate, currentTime) { return; } + const userDefaultBreakMinutes = Number.isInteger(defaultBreakMinutes) && defaultBreakMinutes >= 0 + ? defaultBreakMinutes + : 30; + if (isReachable) { // IP ist erreichbar if (!pingStatus) { @@ -67,9 +71,9 @@ async function pingUserIP(userId, ip, currentDate, currentTime) { }); } else if (!entry) { // Kein Eintrag existiert → Erstelle neuen mit start_time - db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, - [userId, currentDate, currentTime], (err) => { + db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + [userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => { if (err) { console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err); } else { @@ -161,7 +165,7 @@ function setupPingService() { const currentTime = getCurrentTime(); // Hole alle User mit IP-Adresse - db.all('SELECT id, ping_ip FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => { + db.all('SELECT id, ping_ip, default_break_minutes FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => { if (err) { console.error('Fehler beim Abrufen der User mit IP-Adressen:', err); return; @@ -173,7 +177,7 @@ function setupPingService() { // Ping alle User parallel users.forEach(user => { - pingUserIP(user.id, user.ping_ip, currentDate, currentTime); + pingUserIP(user.id, user.ping_ip, user.default_break_minutes, currentDate, currentTime); }); }); }, 60000); // Jede Minute diff --git a/views/overtime-breakdown.ejs b/views/overtime-breakdown.ejs index 1fc1913..a5a0484 100644 --- a/views/overtime-breakdown.ejs +++ b/views/overtime-breakdown.ejs @@ -145,15 +145,11 @@ Davon genommen: -
-
- Verbleibend: - - -
-
Lade Daten...
@@ -323,12 +323,16 @@ corrections.forEach(c => { const dt = parseSqliteDatetime(c.corrected_at); const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; - const hoursText = formatHoursMin(c.correction_hours); + const rawHours = Number(c.correction_hours) || 0; + const absHours = Math.abs(rawHours); + const signPrefix = rawHours >= 0 ? '+' : '-'; + const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative'; + const hoursDisplay = signPrefix + formatHoursMin(absHours); const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const li = document.createElement('li'); - li.textContent = reason - ? `Korrektur am ${dateText} ${hoursText} – ${reason}` - : `Korrektur am ${dateText} ${hoursText}`; + li.innerHTML = reason + ? `Korrektur am ${dateText} ${hoursDisplay} – ${reason}` + : `Korrektur am ${dateText} ${hoursDisplay}`; correctionsListEl.appendChild(li); }); diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index 3ee1559..00fb0b5 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -645,12 +645,16 @@ corrections.forEach(c => { const dt = parseSqliteDatetime(c.corrected_at); const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; - const hoursText = formatHoursMin(c.correction_hours); + const rawHours = Number(c.correction_hours) || 0; + const absHours = Math.abs(rawHours); + const signPrefix = rawHours >= 0 ? '+' : '-'; + const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative'; + const hoursDisplay = signPrefix + formatHoursMin(absHours); const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const li = document.createElement('li'); - li.textContent = reason - ? `Korrektur am ${dateText} ${hoursText} – ${reason}` - : `Korrektur am ${dateText} ${hoursText}`; + li.innerHTML = reason + ? `Korrektur am ${dateText} ${hoursDisplay} – ${reason}` + : `Korrektur am ${dateText} ${hoursDisplay}`; if (listEl) listEl.appendChild(li); }); } catch (e) {