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') %> |
|