diff --git a/database.js b/database.js index 01401a8..d6324a0 100644 --- a/database.js +++ b/database.js @@ -26,6 +26,13 @@ function initDatabase() { // Fehler ignorieren wenn Spalte bereits existiert }); + // Migration: default_break_minutes (Standard-Pausenzeit pro Mitarbeiter) + db.run(`ALTER TABLE users ADD COLUMN default_break_minutes INTEGER DEFAULT 30`, (err) => { + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte default_break_minutes:', err.message); + } + }); + // Stundenerfassung-Tabelle db.run(`CREATE TABLE IF NOT EXISTS timesheet_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/public/css/style.css b/public/css/style.css index 06bb236..b6312b2 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1100,6 +1100,11 @@ table input[type="text"] { } } +/* Pausenfeld: rot nur wenn unter gesetzlicher Mindestpause (Tooltip im HTML) */ +input.break-below-legal { + color: #dc3545; +} + /* App Footer */ .app-footer { text-align: center; diff --git a/public/js/admin.js b/public/js/admin.js index f27c4d7..e1c5a69 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -17,6 +17,10 @@ document.addEventListener('DOMContentLoaded', function() { return; } + const defaultBreakInput = document.getElementById('defaultBreakMinutes'); + const defaultBreakVal = defaultBreakInput && defaultBreakInput.value !== '' ? parseInt(defaultBreakInput.value, 10) : 30; + const default_break_minutes = (!isNaN(defaultBreakVal) && defaultBreakVal >= 0) ? defaultBreakVal : 30; + const formData = { username: document.getElementById('username').value, password: document.getElementById('password').value, @@ -26,7 +30,8 @@ document.addEventListener('DOMContentLoaded', function() { personalnummer: document.getElementById('personalnummer').value, wochenstunden: document.getElementById('wochenstunden').value, arbeitstage: document.getElementById('arbeitstage').value, - urlaubstage: document.getElementById('urlaubstage').value + urlaubstage: document.getElementById('urlaubstage').value, + default_break_minutes: default_break_minutes }; try { @@ -318,6 +323,9 @@ async function saveUser(userId) { const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value; const arbeitstage = row.querySelector('input[data-field="arbeitstage"]').value; const urlaubstage = row.querySelector('input[data-field="urlaubstage"]').value; + const defaultBreakInput = row.querySelector('input[data-field="default_break_minutes"]'); + const default_break_minutes = defaultBreakInput && defaultBreakInput.value !== '' ? parseInt(defaultBreakInput.value, 10) : 30; + const normalizedDefaultBreak = (!isNaN(default_break_minutes) && default_break_minutes >= 0) ? default_break_minutes : 30; // Rollen aus Checkboxen sammeln const roleCheckboxes = row.querySelectorAll('.role-checkbox:checked'); @@ -340,6 +348,7 @@ async function saveUser(userId) { wochenstunden: wochenstunden || null, arbeitstage: arbeitstage || 5, urlaubstage: urlaubstage || null, + default_break_minutes: normalizedDefaultBreak, roles: roles }) }); @@ -351,6 +360,8 @@ async function saveUser(userId) { row.querySelector('span[data-field="personalnummer"]').textContent = personalnummer || '-'; row.querySelector('span[data-field="wochenstunden"]').textContent = wochenstunden || '-'; row.querySelector('span[data-field="urlaubstage"]').textContent = urlaubstage || '-'; + const defaultBreakDisplay = row.querySelector('span[data-field="default_break_minutes"]'); + if (defaultBreakDisplay) defaultBreakDisplay.textContent = normalizedDefaultBreak; // Rollen-Display aktualisieren const rolesDisplay = row.querySelector('div[data-field="roles"]'); diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 967fca7..496987d 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -6,6 +6,7 @@ let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-D let userWochenstunden = 0; // Wochenstunden des Users let userArbeitstage = 5; // Arbeitstage pro Woche des Users (Standard: 5) let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal) +let defaultBreakMinutes = 30; // Standard-Pausenzeit des Mitarbeiters (Vorbelegung) let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version // Wochenend-Prozentsätze laden @@ -376,16 +377,18 @@ function getFullDayHours() { // Woche laden async function loadWeek() { try { - // User-Daten laden (Wochenstunden, Arbeitstage) + // User-Daten laden (Wochenstunden, Arbeitstage, Standard-Pausenzeit) try { const userResponse = await fetch('/api/user/data'); const userData = await userResponse.json(); userWochenstunden = userData.wochenstunden || 0; userArbeitstage = userData.arbeitstage || 5; + defaultBreakMinutes = userData.default_break_minutes ?? 30; } catch (error) { console.warn('Konnte User-Daten nicht laden:', error); userWochenstunden = 0; userArbeitstage = 5; + defaultBreakMinutes = 30; } const parts = currentWeekStart.split('-'); @@ -472,7 +475,7 @@ function renderWeek() { const startTime = entry.start_time || ''; const endTime = entry.end_time || ''; - const breakMinutes = entry.break_minutes || 0; + const breakMinutes = (entry.break_minutes != null && entry.break_minutes !== '') ? entry.break_minutes : defaultBreakMinutes; const hours = entry.total_hours || 0; const overtimeTaken = entry.overtime_taken_hours || ''; const vacationType = entry.vacation_type || ''; @@ -568,24 +571,29 @@ function renderWeek() { hoursDisplay = hours.toFixed(2) + ' h'; } + const requiredBreak = (startTime && endTime) ? calculateRequiredBreakMinutes(startTime, endTime) : null; + const isBreakBelowLegal = requiredBreak !== null && breakMinutes < requiredBreak; + const breakClass = isBreakBelowLegal ? 'break-below-legal' : ''; + const breakTitle = isBreakBelowLegal ? ' title="Die Pausenzeit liegt unterhalb der gesetzlichen Vorgabe."' : ''; + return ` ${getWeekday(dateStr)} ${formatDateDE(dateStr)}${isFullDayVacation ? ' (Urlaub - ganzer Tag)' : ''}${isSick ? ' (Krank)' : ''}${holidayLabel} - - - @@ -1087,6 +1095,26 @@ function calculateRequiredBreakMinutes(startTime, endTime) { return 0; // Weniger als 6 Stunden: keine gesetzliche Pause erforderlich } +// Aktualisiert die visuelle Kennzeichnung (nur rot + Tooltip wenn unter gesetzlicher Mindestpause) +function updateBreakCompliance(dateStr) { + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`); + if (!breakInput) return; + breakInput.classList.remove('break-below-legal'); + breakInput.removeAttribute('title'); + const startTime = startInput && startInput.value ? startInput.value.trim() : ''; + const endTime = endInput && endInput.value ? endInput.value.trim() : ''; + if (!startTime || !endTime) return; + const required = calculateRequiredBreakMinutes(startTime, endTime); + if (required === null) return; + const breakVal = breakInput.value ? (parseInt(breakInput.value, 10) || 0) : 0; + if (breakVal < required) { + breakInput.classList.add('break-below-legal'); + breakInput.setAttribute('title', 'Die Pausenzeit liegt unterhalb der gesetzlichen Vorgabe.'); + } +} + // Eintrag speichern async function saveEntry(input) { const date = input.dataset.date; @@ -1133,19 +1161,6 @@ async function saveEntry(input) { const end_time = actualEndTime; let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0); - // Automatische Vorbelegung der Pausenzeiten basierend auf gesetzlichen Vorgaben - // Wird ausgelöst, wenn start_time oder end_time geändert werden - if ((input.dataset.field === 'start_time' || input.dataset.field === 'end_time') && start_time && end_time) { - const requiredBreakMinutes = calculateRequiredBreakMinutes(start_time, end_time); - if (requiredBreakMinutes !== null && requiredBreakMinutes > break_minutes) { - // Setze den höheren Wert (gesetzliche Mindestpause) - break_minutes = requiredBreakMinutes; - // Aktualisiere das Input-Feld im DOM - if (breakInput) { - breakInput.value = break_minutes; - } - } - } const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || ''); const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null); const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null); @@ -1490,6 +1505,10 @@ async function saveEntry(input) { // Submit-Button Status prüfen (nach jedem Speichern) checkWeekComplete(); + if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') { + updateBreakCompliance(date); + } + // Visuelles Feedback input.style.backgroundColor = '#d4edda'; setTimeout(() => { diff --git a/routes/admin-routes.js b/routes/admin-routes.js index 347d2fb..68746a5 100644 --- a/routes/admin-routes.js +++ b/routes/admin-routes.js @@ -8,7 +8,7 @@ const { requireAdmin } = require('../middleware/auth'); function registerAdminRoutes(app) { // Admin-Bereich app.get('/admin', requireAdmin, (req, res) => { - db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, created_at FROM users ORDER BY created_at DESC', + db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, default_break_minutes, created_at FROM users ORDER BY created_at DESC', (err, users) => { // LDAP-Konfiguration, Sync-Log und Optionen abrufen db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => { @@ -48,7 +48,7 @@ function registerAdminRoutes(app) { // Benutzer erstellen app.post('/admin/users', requireAdmin, (req, res) => { - const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage, arbeitstage } = req.body; + const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage, arbeitstage, default_break_minutes } = req.body; const hashedPassword = bcrypt.hashSync(password, 10); // Normalisiere die optionalen Felder @@ -56,6 +56,8 @@ function registerAdminRoutes(app) { const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null; const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null; const normalizedArbeitstage = arbeitstage && arbeitstage !== '' ? parseInt(arbeitstage) : 5; + const parsedBreak = default_break_minutes !== undefined && default_break_minutes !== '' ? parseInt(default_break_minutes, 10) : 30; + const normalizedDefaultBreak = (!isNaN(parsedBreak) && parsedBreak >= 0) ? parsedBreak : 30; // Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String let rolesArray = []; @@ -73,8 +75,8 @@ function registerAdminRoutes(app) { const rolesJson = JSON.stringify(rolesArray); - db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', - [username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage, normalizedArbeitstage], + db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, default_break_minutes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage, normalizedArbeitstage, normalizedDefaultBreak], (err) => { if (err) { return res.status(400).json({ error: 'Benutzername existiert bereits' }); @@ -100,10 +102,13 @@ function registerAdminRoutes(app) { }); }); - // Benutzer aktualisieren (Personalnummer, Wochenstunden, Urlaubstage, Rollen) + // Benutzer aktualisieren (Personalnummer, Wochenstunden, Urlaubstage, Rollen, Standard-Pause) app.put('/admin/users/:id', requireAdmin, (req, res) => { const userId = req.params.id; - const { personalnummer, wochenstunden, urlaubstage, arbeitstage, roles } = req.body; + const { personalnummer, wochenstunden, urlaubstage, arbeitstage, roles, default_break_minutes } = req.body; + + const parsedBreak = default_break_minutes !== undefined && default_break_minutes !== '' ? parseInt(default_break_minutes, 10) : 30; + const normalizedDefaultBreak = (!isNaN(parsedBreak) && parsedBreak >= 0) ? parsedBreak : 30; // Rollen verarbeiten falls vorhanden let rolesJson = null; @@ -122,12 +127,13 @@ function registerAdminRoutes(app) { // SQL-Query dynamisch zusammenstellen if (rolesJson !== null) { // Aktualisiere auch Rollen - db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ?, role = ? WHERE id = ?', + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ?, default_break_minutes = ?, role = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, arbeitstage ? parseInt(arbeitstage) : 5, + normalizedDefaultBreak, rolesJson, userId ], @@ -139,12 +145,13 @@ function registerAdminRoutes(app) { }); } else { // Nur andere Felder aktualisieren - db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ? WHERE id = ?', + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ?, default_break_minutes = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, arbeitstage ? parseInt(arbeitstage) : 5, + normalizedDefaultBreak, userId ], (err) => { diff --git a/routes/user-routes.js b/routes/user-routes.js index a955f26..2fa7fd7 100644 --- a/routes/user-routes.js +++ b/routes/user-routes.js @@ -57,14 +57,15 @@ function registerUserRoutes(app) { app.get('/api/user/data', requireAuth, (req, res) => { const userId = req.session.userId; - 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) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } res.json({ wochenstunden: user?.wochenstunden || 0, - arbeitstage: user?.arbeitstage || 5 + arbeitstage: user?.arbeitstage || 5, + default_break_minutes: user?.default_break_minutes ?? 30 }); }); }); diff --git a/views/admin.ejs b/views/admin.ejs index 2a1761b..9889c09 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -107,6 +107,11 @@ +
+ + + Vorbelegung Pausenzeit pro Tag (Min., mind. 0). +
@@ -136,6 +141,7 @@ Wochenstunden Arbeitstage pro Woche Urlaubstage + Standard-Pause (Min) Erstellt am Aktionen @@ -194,6 +200,10 @@ <%= u.urlaubstage || '-' %> + + <%= (u.default_break_minutes != null && u.default_break_minutes !== '') ? u.default_break_minutes : '-' %> + + <%= new Date(u.created_at).toLocaleDateString('de-DE') %>