Compare commits
13 Commits
b0abba5f0f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3827d0451d | |||
| d4a544164b | |||
| bf578a8d87 | |||
| a1c4770de8 | |||
| e21439797f | |||
| 7c81095924 | |||
| 5e188cace2 | |||
| af1f6efb40 | |||
| 23c255438b | |||
| 03b4db8247 | |||
| b6c8ca1df1 | |||
| 3edc0fe60c | |||
| e020aa4e46 |
@@ -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;
|
||||||
@@ -47,7 +47,7 @@ checkinApp.get('/api/checkin/:userId', (req, res) => {
|
|||||||
const currentTime = getCurrentTime();
|
const currentTime = getCurrentTime();
|
||||||
|
|
||||||
// Prüfe ob User existiert
|
// 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) {
|
if (err || !user) {
|
||||||
return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 });
|
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 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) {
|
if (!entry) {
|
||||||
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
||||||
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||||
[userId, currentDate, currentTime], (err) => {
|
[userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 });
|
return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 });
|
||||||
}
|
}
|
||||||
@@ -139,7 +143,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,
|
||||||
|
|||||||
32
database.js
32
database.js
@@ -26,6 +26,13 @@ function initDatabase() {
|
|||||||
// Fehler ignorieren wenn Spalte bereits existiert
|
// 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
|
// Stundenerfassung-Tabelle
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS timesheet_entries (
|
db.run(`CREATE TABLE IF NOT EXISTS timesheet_entries (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -208,6 +215,31 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tabelle: Protokoll für Überstunden-Korrekturen durch Verwaltung
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS overtime_corrections (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
correction_hours REAL NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
corrected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.warn('Warnung beim Erstellen der overtime_corrections Tabelle:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migration: reason Spalte für overtime_corrections hinzufügen (falls Tabelle bereits existiert)
|
||||||
|
db.run(`ALTER TABLE overtime_corrections ADD COLUMN reason TEXT`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
// "duplicate column" ist SQLite CLI wording; sqlite3 node liefert typischerweise "duplicate column name"
|
||||||
|
if (!err.message.includes('duplicate column name')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte reason (overtime_corrections):', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Migration: Urlaubstage-Offset (manuelle Korrektur durch Verwaltung)
|
// Migration: Urlaubstage-Offset (manuelle Korrektur durch Verwaltung)
|
||||||
db.run(`ALTER TABLE users ADD COLUMN vacation_offset_days REAL DEFAULT 0`, (err) => {
|
db.run(`ALTER TABLE users ADD COLUMN vacation_offset_days REAL DEFAULT 0`, (err) => {
|
||||||
// Fehler ignorieren wenn Spalte bereits existiert
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE
|
- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE
|
||||||
- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -> DONE sollte passen
|
- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -> DONE sollte passen
|
||||||
- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE
|
- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE
|
||||||
|
- Grund für Überstundenkorrektur -> DONE
|
||||||
@@ -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 '0h 0min';
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -840,6 +840,17 @@ table input[type="text"] {
|
|||||||
color: #7f8c8d;
|
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/Tätigkeiten */
|
||||||
.activities-row {
|
.activities-row {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@@ -864,7 +875,7 @@ table input[type="text"] {
|
|||||||
|
|
||||||
.activity-row {
|
.activity-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 150px 120px;
|
grid-template-columns: 1fr 150px 150px;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -893,7 +904,7 @@ table input[type="text"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.activity-hours-input {
|
.activity-hours-input {
|
||||||
width: 80px;
|
width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-hours-label {
|
.activity-hours-label {
|
||||||
@@ -901,6 +912,11 @@ table input[type="text"] {
|
|||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-hours-hh-input,
|
||||||
|
.activity-hours-mm-input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-project {
|
.activity-project {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -976,6 +992,20 @@ table input[type="text"] {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.employee-new-version-warning {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e74c3c;
|
||||||
|
background-color: #fee;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
border-radius: 999px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.employee-details {
|
.employee-details {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -1100,6 +1130,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 */
|
||||||
.app-footer {
|
.app-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return;
|
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 = {
|
const formData = {
|
||||||
username: document.getElementById('username').value,
|
username: document.getElementById('username').value,
|
||||||
password: document.getElementById('password').value,
|
password: document.getElementById('password').value,
|
||||||
@@ -26,7 +30,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
personalnummer: document.getElementById('personalnummer').value,
|
personalnummer: document.getElementById('personalnummer').value,
|
||||||
wochenstunden: document.getElementById('wochenstunden').value,
|
wochenstunden: document.getElementById('wochenstunden').value,
|
||||||
arbeitstage: document.getElementById('arbeitstage').value,
|
arbeitstage: document.getElementById('arbeitstage').value,
|
||||||
urlaubstage: document.getElementById('urlaubstage').value
|
urlaubstage: document.getElementById('urlaubstage').value,
|
||||||
|
default_break_minutes: default_break_minutes
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -318,6 +323,9 @@ async function saveUser(userId) {
|
|||||||
const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value;
|
const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value;
|
||||||
const arbeitstage = row.querySelector('input[data-field="arbeitstage"]').value;
|
const arbeitstage = row.querySelector('input[data-field="arbeitstage"]').value;
|
||||||
const urlaubstage = row.querySelector('input[data-field="urlaubstage"]').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
|
// Rollen aus Checkboxen sammeln
|
||||||
const roleCheckboxes = row.querySelectorAll('.role-checkbox:checked');
|
const roleCheckboxes = row.querySelectorAll('.role-checkbox:checked');
|
||||||
@@ -340,6 +348,7 @@ async function saveUser(userId) {
|
|||||||
wochenstunden: wochenstunden || null,
|
wochenstunden: wochenstunden || null,
|
||||||
arbeitstage: arbeitstage || 5,
|
arbeitstage: arbeitstage || 5,
|
||||||
urlaubstage: urlaubstage || null,
|
urlaubstage: urlaubstage || null,
|
||||||
|
default_break_minutes: normalizedDefaultBreak,
|
||||||
roles: roles
|
roles: roles
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -351,6 +360,8 @@ async function saveUser(userId) {
|
|||||||
row.querySelector('span[data-field="personalnummer"]').textContent = personalnummer || '-';
|
row.querySelector('span[data-field="personalnummer"]').textContent = personalnummer || '-';
|
||||||
row.querySelector('span[data-field="wochenstunden"]').textContent = wochenstunden || '-';
|
row.querySelector('span[data-field="wochenstunden"]').textContent = wochenstunden || '-';
|
||||||
row.querySelector('span[data-field="urlaubstage"]').textContent = urlaubstage || '-';
|
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
|
// Rollen-Display aktualisieren
|
||||||
const rolesDisplay = row.querySelector('div[data-field="roles"]');
|
const rolesDisplay = row.querySelector('div[data-field="roles"]');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-D
|
|||||||
let userWochenstunden = 0; // Wochenstunden des Users
|
let userWochenstunden = 0; // Wochenstunden des Users
|
||||||
let userArbeitstage = 5; // Arbeitstage pro Woche des Users (Standard: 5)
|
let userArbeitstage = 5; // Arbeitstage pro Woche des Users (Standard: 5)
|
||||||
let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal)
|
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
|
let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version
|
||||||
|
|
||||||
// Wochenend-Prozentsätze laden
|
// Wochenend-Prozentsätze laden
|
||||||
@@ -50,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');
|
||||||
@@ -373,19 +374,67 @@ function getFullDayHours() {
|
|||||||
return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8;
|
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
|
// Woche laden
|
||||||
async function loadWeek() {
|
async function loadWeek() {
|
||||||
try {
|
try {
|
||||||
// User-Daten laden (Wochenstunden, Arbeitstage)
|
// User-Daten laden (Wochenstunden, Arbeitstage, Standard-Pausenzeit)
|
||||||
try {
|
try {
|
||||||
const userResponse = await fetch('/api/user/data');
|
const userResponse = await fetch('/api/user/data');
|
||||||
const userData = await userResponse.json();
|
const userData = await userResponse.json();
|
||||||
userWochenstunden = userData.wochenstunden || 0;
|
userWochenstunden = userData.wochenstunden || 0;
|
||||||
userArbeitstage = userData.arbeitstage || 5;
|
userArbeitstage = userData.arbeitstage || 5;
|
||||||
|
defaultBreakMinutes = userData.default_break_minutes ?? 30;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Konnte User-Daten nicht laden:', error);
|
console.warn('Konnte User-Daten nicht laden:', error);
|
||||||
userWochenstunden = 0;
|
userWochenstunden = 0;
|
||||||
userArbeitstage = 5;
|
userArbeitstage = 5;
|
||||||
|
defaultBreakMinutes = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = currentWeekStart.split('-');
|
const parts = currentWeekStart.split('-');
|
||||||
@@ -472,7 +521,7 @@ function renderWeek() {
|
|||||||
|
|
||||||
const startTime = entry.start_time || '';
|
const startTime = entry.start_time || '';
|
||||||
const endTime = entry.end_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 hours = entry.total_hours || 0;
|
||||||
const overtimeTaken = entry.overtime_taken_hours || '';
|
const overtimeTaken = entry.overtime_taken_hours || '';
|
||||||
const vacationType = entry.vacation_type || '';
|
const vacationType = entry.vacation_type || '';
|
||||||
@@ -545,47 +594,52 @@ 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 isBreakBelowLegal = requiredBreak !== null && breakMinutes < requiredBreak;
|
||||||
|
const breakClass = isBreakBelowLegal ? 'break-below-legal' : '';
|
||||||
|
const breakTitle = isBreakBelowLegal ? ' title="Die Pausenzeit liegt unterhalb der gesetzlichen Vorgabe."' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>${getWeekday(dateStr)}</strong></td>
|
<td><strong>${getWeekday(dateStr)}</strong></td>
|
||||||
<td>${formatDateDE(dateStr)}${isFullDayVacation ? ' <span style="color: #28a745;">(Urlaub - ganzer Tag)</span>' : ''}${isSick ? ' <span style="color: #e74c3c;">(Krank)</span>' : ''}${holidayLabel}</td>
|
<td>${formatDateDE(dateStr)}${isFullDayVacation ? ' <span style="color: #28a745;">(Urlaub - ganzer Tag)</span>' : ''}${isSick ? ' <span style="color: #e74c3c;">(Krank)</span>' : ''}${holidayLabel}</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="time" value="${startTime}"
|
<input type="time" id="start_time_${dateStr}" name="start_time_${dateStr}" value="${startTime}"
|
||||||
data-date="${dateStr}" data-field="start_time"
|
data-date="${dateStr}" data-field="start_time"
|
||||||
step="60"
|
step="60"
|
||||||
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
|
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="time" value="${endTime}"
|
<input type="time" id="end_time_${dateStr}" name="end_time_${dateStr}" value="${endTime}"
|
||||||
data-date="${dateStr}" data-field="end_time"
|
data-date="${dateStr}" data-field="end_time"
|
||||||
step="60"
|
step="60"
|
||||||
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
|
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="number" value="${breakMinutes}" min="0" step="15"
|
<input type="number" id="break_minutes_${dateStr}" name="break_minutes_${dateStr}" value="${breakMinutes}" min="0" step="15" class="${breakClass}"${breakTitle}
|
||||||
data-date="${dateStr}" data-field="break_minutes"
|
data-date="${dateStr}" data-field="break_minutes"
|
||||||
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)">
|
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)">
|
||||||
</td>
|
</td>
|
||||||
@@ -595,7 +649,9 @@ function renderWeek() {
|
|||||||
<td colspan="6" class="activities-cell">
|
<td colspan="6" class="activities-cell">
|
||||||
<div class="activities-form">
|
<div class="activities-form">
|
||||||
<div class="activities-header"><strong>Tätigkeiten:</strong></div>
|
<div class="activities-header"><strong>Tätigkeiten:</strong></div>
|
||||||
${activities.map((activity, idx) => `
|
${activities.map((activity, idx) => {
|
||||||
|
const timeParts = decimalHoursToParts(activity.hours);
|
||||||
|
return `
|
||||||
<div class="activity-row">
|
<div class="activity-row">
|
||||||
<div class="activity-desc">
|
<div class="activity-desc">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -618,22 +674,36 @@ function renderWeek() {
|
|||||||
class="activity-project-input">
|
class="activity-project-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-hours">
|
<div class="activity-hours">
|
||||||
<input type="number"
|
<input type="text"
|
||||||
data-date="${dateStr}"
|
data-date="${dateStr}"
|
||||||
data-field="activity${idx + 1}_hours"
|
data-field="activity${idx + 1}_hours_hh"
|
||||||
value="${activity.hours > 0 ? activity.hours.toFixed(2) : ''}"
|
value="${timeParts.hh}"
|
||||||
min="0"
|
inputmode="numeric"
|
||||||
step="0.25"
|
pattern="[0-9]*"
|
||||||
placeholder="0.00"
|
placeholder="hh"
|
||||||
${timeFieldsDisabled} ${disabled}
|
${timeFieldsDisabled} ${disabled}
|
||||||
onblur="saveEntry(this)"
|
onblur="saveEntry(this)"
|
||||||
oninput="updateOvertimeDisplay();"
|
oninput="updateOvertimeDisplay();"
|
||||||
onchange="updateOvertimeDisplay();"
|
onchange="updateOvertimeDisplay();"
|
||||||
class="activity-hours-input">
|
class="activity-hours-input activity-hours-hh-input">
|
||||||
<span class="activity-hours-label">h</span>
|
<span class="activity-hours-label">h</span>
|
||||||
|
<input type="text"
|
||||||
|
data-date="${dateStr}"
|
||||||
|
data-field="activity${idx + 1}_hours_mm"
|
||||||
|
value="${timeParts.mm}"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
placeholder="mm"
|
||||||
|
${timeFieldsDisabled} ${disabled}
|
||||||
|
onblur="saveEntry(this)"
|
||||||
|
oninput="updateOvertimeDisplay();"
|
||||||
|
onchange="updateOvertimeDisplay();"
|
||||||
|
class="activity-hours-input activity-hours-mm-input">
|
||||||
|
<span class="activity-hours-label">min</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
<div class="overtime-vacation-controls" style="margin-top: 15px; display: flex; gap: 15px; align-items: center;">
|
<div class="overtime-vacation-controls" style="margin-top: 15px; display: flex; gap: 15px; align-items: center;">
|
||||||
<div class="overtime-control">
|
<div class="overtime-control">
|
||||||
@@ -733,7 +803,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)
|
||||||
|
|
||||||
@@ -995,13 +1065,9 @@ function updateOvertimeDisplay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überstunden berechnen (wie im Backend: mit adjustedSollStunden)
|
// Variante B: Überstunden/Fehlstunden = Gesamt − Soll (Soll immer vertraglich)
|
||||||
// totalHours enthält bereits Feiertagsstunden (8h oder gearbeitete Stunden) aus dem Feiertag-Zweig oben
|
|
||||||
const totalHoursWithVacation = totalHours + vacationHours;
|
const totalHoursWithVacation = totalHours + vacationHours;
|
||||||
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
const overtimeHours = totalHoursWithVacation - sollStunden;
|
||||||
// overtimeHours = Überstunden diese Woche (wie im Backend berechnet)
|
|
||||||
// Genommene Überstunden werden abgezogen, um die Netto-Überstunden zu erhalten
|
|
||||||
const overtimeHours = totalHoursWithVacation - adjustedSollStunden - overtimeTaken;
|
|
||||||
|
|
||||||
// Überstunden-Anzeige aktualisieren
|
// Überstunden-Anzeige aktualisieren
|
||||||
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
|
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
|
||||||
@@ -1009,14 +1075,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,7 +1100,8 @@ function handleOvertimeChange(dateStr, overtimeHours) {
|
|||||||
// (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit)
|
// (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit)
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
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') {
|
if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') {
|
||||||
descInput.value = '';
|
descInput.value = '';
|
||||||
@@ -1043,6 +1110,10 @@ function handleOvertimeChange(dateStr, overtimeHours) {
|
|||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
saveEntry(hoursInput);
|
saveEntry(hoursInput);
|
||||||
}
|
}
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
saveEntry(minutesInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,6 +1158,26 @@ function calculateRequiredBreakMinutes(startTime, endTime) {
|
|||||||
return 0; // Weniger als 6 Stunden: keine gesetzliche Pause erforderlich
|
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
|
// Eintrag speichern
|
||||||
async function saveEntry(input) {
|
async function saveEntry(input) {
|
||||||
const date = input.dataset.date;
|
const date = input.dataset.date;
|
||||||
@@ -1131,21 +1222,19 @@ async function saveEntry(input) {
|
|||||||
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
||||||
const start_time = actualStartTime;
|
const start_time = actualStartTime;
|
||||||
const end_time = actualEndTime;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 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 notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
|
||||||
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
|
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);
|
const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null);
|
||||||
@@ -1157,12 +1246,16 @@ async function saveEntry(input) {
|
|||||||
const activities = [];
|
const activities = [];
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
const fallbackHours = parseFloat(currentEntries[date][`activity${i}_hours`]) || 0;
|
||||||
|
|
||||||
activities.push({
|
activities.push({
|
||||||
desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null),
|
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)
|
projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1350,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
|
||||||
@@ -1278,9 +1371,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 {
|
||||||
@@ -1288,15 +1381,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1326,21 +1419,27 @@ async function saveEntry(input) {
|
|||||||
// 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere
|
// 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere
|
||||||
if (isFullDayVacation) {
|
if (isFullDayVacation) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_desc"]`);
|
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) {
|
if (descInput) {
|
||||||
descInput.value = 'Urlaub';
|
descInput.value = 'Urlaub';
|
||||||
currentEntries[date].activity1_desc = 'Urlaub';
|
currentEntries[date].activity1_desc = 'Urlaub';
|
||||||
}
|
}
|
||||||
if (hoursInput) {
|
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;
|
currentEntries[date].activity1_hours = fullDayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leere andere Tätigkeiten
|
// Leere andere Tätigkeiten
|
||||||
for (let i = 2; i <= 5; i++) {
|
for (let i = 2; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -1349,6 +1448,9 @@ async function saveEntry(input) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[date][`activity${i}_hours`] = 0;
|
currentEntries[date][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -1360,7 +1462,8 @@ async function saveEntry(input) {
|
|||||||
// Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren
|
// Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -1369,6 +1472,9 @@ async function saveEntry(input) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[date][`activity${i}_hours`] = 0;
|
currentEntries[date][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -1431,10 +1537,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
|
||||||
@@ -1443,25 +1549,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.)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1477,7 +1583,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();
|
||||||
@@ -1490,6 +1596,10 @@ async function saveEntry(input) {
|
|||||||
// Submit-Button Status prüfen (nach jedem Speichern)
|
// Submit-Button Status prüfen (nach jedem Speichern)
|
||||||
checkWeekComplete();
|
checkWeekComplete();
|
||||||
|
|
||||||
|
if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') {
|
||||||
|
updateBreakCompliance(date);
|
||||||
|
}
|
||||||
|
|
||||||
// Visuelles Feedback
|
// Visuelles Feedback
|
||||||
input.style.backgroundColor = '#d4edda';
|
input.style.backgroundColor = '#d4edda';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1572,7 +1682,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2058,22 +2168,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2103,7 +2213,8 @@ function toggleSickStatus(dateStr) {
|
|||||||
// Leere alle Tätigkeitsfelder
|
// Leere alle Tätigkeitsfelder
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
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"]`);
|
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2112,6 +2223,9 @@ function toggleSickStatus(dateStr) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -2122,7 +2236,8 @@ function toggleSickStatus(dateStr) {
|
|||||||
} else {
|
} else {
|
||||||
// Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere
|
// Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`);
|
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();
|
const fullDayHours = getFullDayHours();
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2130,14 +2245,19 @@ function toggleSickStatus(dateStr) {
|
|||||||
currentEntries[dateStr].activity1_desc = 'Krank';
|
currentEntries[dateStr].activity1_desc = 'Krank';
|
||||||
}
|
}
|
||||||
if (hoursInput) {
|
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;
|
currentEntries[dateStr].activity1_hours = fullDayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leere andere Tätigkeiten
|
// Leere andere Tätigkeiten
|
||||||
for (let i = 2; i <= 5; i++) {
|
for (let i = 2; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
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"]`);
|
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2146,6 +2266,9 @@ function toggleSickStatus(dateStr) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
|
|||||||
79
public/js/format-hours.js
Normal file
79
public/js/format-hours.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// 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 '0h 0min';
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dezimalstunden in h:mm (z. B. 1.5 -> "1:30", -0.75 -> "-0:45") */
|
||||||
|
function decimalHoursToHhMm(decimalHours) {
|
||||||
|
if (decimalHours == null || !Number.isFinite(Number(decimalHours))) return '0:00';
|
||||||
|
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 + ':' + String(min).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst h:mm oder Xh Ymin zu Dezimalstunden.
|
||||||
|
* Beispiele: "1:30" -> 1.5, "-0:45" -> -0.75, "1h 30min" -> 1.5.
|
||||||
|
* @returns {number|null} Dezimalstunden oder null bei ungültiger Eingabe
|
||||||
|
*/
|
||||||
|
function parseHoursMin(str) {
|
||||||
|
if (str == null) return null;
|
||||||
|
var s = String(str).trim();
|
||||||
|
if (s === '') return 0;
|
||||||
|
var sign = 1;
|
||||||
|
if (s.charAt(0) === '-') {
|
||||||
|
sign = -1;
|
||||||
|
s = s.slice(1).trim();
|
||||||
|
} else if (s.charAt(0) === '+') {
|
||||||
|
s = s.slice(1).trim();
|
||||||
|
}
|
||||||
|
// h:mm oder h:mm:ss
|
||||||
|
var colonMatch = s.match(/^(\d+):(\d{1,2})(?::(\d{1,2}))?$/);
|
||||||
|
if (colonMatch) {
|
||||||
|
var hours = parseInt(colonMatch[1], 10);
|
||||||
|
var minutes = parseInt(colonMatch[2], 10);
|
||||||
|
if (minutes >= 60) return null;
|
||||||
|
var sec = colonMatch[3] != null ? parseInt(colonMatch[3], 10) : 0;
|
||||||
|
if (sec >= 60) return null;
|
||||||
|
var decimal = hours + minutes / 60 + sec / 3600;
|
||||||
|
return sign * decimal;
|
||||||
|
}
|
||||||
|
// Xh Ymin (optional Leerzeichen)
|
||||||
|
var hmMatch = s.match(/^(\d+)\s*h\s*(\d{1,2})?\s*min$/i);
|
||||||
|
if (hmMatch) {
|
||||||
|
var h = parseInt(hmMatch[1], 10);
|
||||||
|
var m = (hmMatch[2] != null) ? parseInt(hmMatch[2], 10) : 0;
|
||||||
|
if (m >= 60) return null;
|
||||||
|
return sign * (h + m / 60);
|
||||||
|
}
|
||||||
|
// Nur Zahl (Dezimalstunden) zulassen als Fallback
|
||||||
|
var num = parseFloat(s.replace(',', '.'));
|
||||||
|
if (!Number.isFinite(num)) return null;
|
||||||
|
return sign * num;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.formatHoursMin = formatHoursMin;
|
||||||
|
window.decimalHoursToHhMm = decimalHoursToHhMm;
|
||||||
|
window.parseHoursMin = parseHoursMin;
|
||||||
|
})();
|
||||||
@@ -8,7 +8,7 @@ const { requireAdmin } = require('../middleware/auth');
|
|||||||
function registerAdminRoutes(app) {
|
function registerAdminRoutes(app) {
|
||||||
// Admin-Bereich
|
// Admin-Bereich
|
||||||
app.get('/admin', requireAdmin, (req, res) => {
|
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) => {
|
(err, users) => {
|
||||||
// LDAP-Konfiguration, Sync-Log und Optionen abrufen
|
// LDAP-Konfiguration, Sync-Log und Optionen abrufen
|
||||||
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
|
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
|
||||||
@@ -48,7 +48,7 @@ function registerAdminRoutes(app) {
|
|||||||
|
|
||||||
// Benutzer erstellen
|
// Benutzer erstellen
|
||||||
app.post('/admin/users', requireAdmin, (req, res) => {
|
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);
|
const hashedPassword = bcrypt.hashSync(password, 10);
|
||||||
|
|
||||||
// Normalisiere die optionalen Felder
|
// Normalisiere die optionalen Felder
|
||||||
@@ -56,6 +56,8 @@ function registerAdminRoutes(app) {
|
|||||||
const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null;
|
const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null;
|
||||||
const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null;
|
const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null;
|
||||||
const normalizedArbeitstage = arbeitstage && arbeitstage !== '' ? parseInt(arbeitstage) : 5;
|
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
|
// Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String
|
||||||
let rolesArray = [];
|
let rolesArray = [];
|
||||||
@@ -73,8 +75,8 @@ function registerAdminRoutes(app) {
|
|||||||
|
|
||||||
const rolesJson = JSON.stringify(rolesArray);
|
const rolesJson = JSON.stringify(rolesArray);
|
||||||
|
|
||||||
db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
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],
|
[username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage, normalizedArbeitstage, normalizedDefaultBreak],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(400).json({ error: 'Benutzername existiert bereits' });
|
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) => {
|
app.put('/admin/users/:id', requireAdmin, (req, res) => {
|
||||||
const userId = req.params.id;
|
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
|
// Rollen verarbeiten falls vorhanden
|
||||||
let rolesJson = null;
|
let rolesJson = null;
|
||||||
@@ -122,12 +127,13 @@ function registerAdminRoutes(app) {
|
|||||||
// SQL-Query dynamisch zusammenstellen
|
// SQL-Query dynamisch zusammenstellen
|
||||||
if (rolesJson !== null) {
|
if (rolesJson !== null) {
|
||||||
// Aktualisiere auch Rollen
|
// 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,
|
personalnummer || null,
|
||||||
wochenstunden ? parseFloat(wochenstunden) : null,
|
wochenstunden ? parseFloat(wochenstunden) : null,
|
||||||
urlaubstage ? parseFloat(urlaubstage) : null,
|
urlaubstage ? parseFloat(urlaubstage) : null,
|
||||||
arbeitstage ? parseInt(arbeitstage) : 5,
|
arbeitstage ? parseInt(arbeitstage) : 5,
|
||||||
|
normalizedDefaultBreak,
|
||||||
rolesJson,
|
rolesJson,
|
||||||
userId
|
userId
|
||||||
],
|
],
|
||||||
@@ -139,12 +145,13 @@ function registerAdminRoutes(app) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Nur andere Felder aktualisieren
|
// 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,
|
personalnummer || null,
|
||||||
wochenstunden ? parseFloat(wochenstunden) : null,
|
wochenstunden ? parseFloat(wochenstunden) : null,
|
||||||
urlaubstage ? parseFloat(urlaubstage) : null,
|
urlaubstage ? parseFloat(urlaubstage) : null,
|
||||||
arbeitstage ? parseInt(arbeitstage) : 5,
|
arbeitstage ? parseInt(arbeitstage) : 5,
|
||||||
|
normalizedDefaultBreak,
|
||||||
userId
|
userId
|
||||||
],
|
],
|
||||||
(err) => {
|
(err) => {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ function registerTimesheetRoutes(app) {
|
|||||||
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
||||||
} = req.body;
|
} = req.body;
|
||||||
const userId = req.session.userId;
|
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
|
// 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);
|
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)
|
// 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) {
|
if (err) {
|
||||||
console.error('Fehler beim Laden der User-Daten:', err);
|
console.error('Fehler beim Laden der User-Daten:', err);
|
||||||
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
|
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 wochenstunden = user?.wochenstunden || 0;
|
||||||
const arbeitstage = user?.arbeitstage || 5;
|
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 overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0;
|
||||||
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 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 start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
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)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
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
|
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
||||||
|
|
||||||
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
// 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) => {
|
[userId, date], (err, row) => {
|
||||||
if (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)
|
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
|
||||||
let finalAppliedPercentage = appliedWeekendPercentage;
|
let finalAppliedPercentage = appliedWeekendPercentage;
|
||||||
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
|
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 start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
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) {
|
if (baseHours > 0 && finalAppliedPercentage >= 100) {
|
||||||
total_hours = baseHours * (finalAppliedPercentage / 100);
|
total_hours = baseHours * (finalAppliedPercentage / 100);
|
||||||
}
|
}
|
||||||
@@ -165,7 +191,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[
|
[
|
||||||
finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes,
|
||||||
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||||
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
||||||
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_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)
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
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,
|
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||||
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
||||||
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
||||||
|
|||||||
@@ -57,14 +57,15 @@ function registerUserRoutes(app) {
|
|||||||
app.get('/api/user/data', requireAuth, (req, res) => {
|
app.get('/api/user/data', requireAuth, (req, res) => {
|
||||||
const userId = req.session.userId;
|
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) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
wochenstunden: user?.wochenstunden || 0,
|
wochenstunden: user?.wochenstunden || 0,
|
||||||
arbeitstage: user?.arbeitstage || 5
|
arbeitstage: user?.arbeitstage || 5,
|
||||||
|
default_break_minutes: user?.default_break_minutes ?? 30
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -392,6 +393,7 @@ function registerUserRoutes(app) {
|
|||||||
const endDate = new Date(week.week_end);
|
const endDate = new Date(week.week_end);
|
||||||
let workdays = 0;
|
let workdays = 0;
|
||||||
let filledWorkdays = 0;
|
let filledWorkdays = 0;
|
||||||
|
const fullDayHoursForCheck = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
|
||||||
|
|
||||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
@@ -407,15 +409,18 @@ function registerUserRoutes(app) {
|
|||||||
// Tag gilt als ausgefüllt wenn:
|
// Tag gilt als ausgefüllt wenn:
|
||||||
// - Ganzer Tag Urlaub (vacation_type = 'full')
|
// - Ganzer Tag Urlaub (vacation_type = 'full')
|
||||||
// - Krank (sick_status = 1)
|
// - Krank (sick_status = 1)
|
||||||
|
// - Ganzer Tag Überstunden (overtime_taken_hours = fullDayHours)
|
||||||
// - ODER Start- und End-Zeit vorhanden sind
|
// - ODER Start- und End-Zeit vorhanden sind
|
||||||
if (entry) {
|
if (entry) {
|
||||||
const isFullDayVacation = entry.vacation_type === 'full';
|
const isFullDayVacation = entry.vacation_type === 'full';
|
||||||
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
||||||
|
const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHoursForCheck) < 0.01;
|
||||||
const hasStartAndEnd = entry.start_time && entry.end_time &&
|
const hasStartAndEnd = entry.start_time && entry.end_time &&
|
||||||
entry.start_time.toString().trim() !== '' &&
|
entry.start_time.toString().trim() !== '' &&
|
||||||
entry.end_time.toString().trim() !== '';
|
entry.end_time.toString().trim() !== '';
|
||||||
|
|
||||||
if (isFullDayVacation || isSick || hasStartAndEnd) {
|
if (isFullDayVacation || isSick || isFullDayOvertime || hasStartAndEnd) {
|
||||||
filledWorkdays++;
|
filledWorkdays++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +466,7 @@ function registerUserRoutes(app) {
|
|||||||
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
|
||||||
|
|
||||||
if (entry.overtime_taken_hours) {
|
if (entry.overtime_taken_hours) {
|
||||||
weekOvertimeTaken += entry.overtime_taken_hours;
|
weekOvertimeTaken += parseFloat(entry.overtime_taken_hours) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden
|
// Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden
|
||||||
@@ -502,19 +507,14 @@ function registerUserRoutes(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sollstunden berechnen
|
// Sollstunden berechnen (Variante B: immer vertraglich, nicht reduziert durch „genommen“)
|
||||||
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
||||||
|
|
||||||
// Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden
|
|
||||||
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
||||||
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
// Überstunden/Fehlstunden = Gesamt − Soll (kann negativ sein)
|
||||||
// weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet)
|
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
|
||||||
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
|
||||||
|
|
||||||
// Kumulativ addieren
|
// Kumulativ addieren
|
||||||
// WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden)
|
|
||||||
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
|
||||||
// Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden
|
|
||||||
totalOvertimeHours += weekOvertimeHours;
|
totalOvertimeHours += weekOvertimeHours;
|
||||||
totalOvertimeTaken += weekOvertimeTaken;
|
totalOvertimeTaken += weekOvertimeTaken;
|
||||||
totalVacationDays += weekVacationDays;
|
totalVacationDays += weekVacationDays;
|
||||||
@@ -523,10 +523,8 @@ function registerUserRoutes(app) {
|
|||||||
|
|
||||||
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
// Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset
|
// Variante B: Verbleibend = Summe Wochen-Überstunden + Offset („genommen“ nur Anzeige)
|
||||||
// weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend)
|
const currentOvertime = totalOvertimeHours + overtimeOffsetHours;
|
||||||
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
|
||||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
|
||||||
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -586,6 +584,17 @@ function registerUserRoutes(app) {
|
|||||||
const arbeitstage = user.arbeitstage || 5;
|
const arbeitstage = user.arbeitstage || 5;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
|
||||||
|
// Korrekturen durch die Verwaltung (Historie) laden
|
||||||
|
db.all(
|
||||||
|
`SELECT correction_hours, corrected_at, reason
|
||||||
|
FROM overtime_corrections
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY corrected_at DESC`,
|
||||||
|
[userId],
|
||||||
|
(correctionsErr, corrections) => {
|
||||||
|
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
|
||||||
|
const overtimeCorrections = correctionsErr ? [] : (corrections || []);
|
||||||
|
|
||||||
// Alle eingereichten Wochen abrufen
|
// Alle eingereichten Wochen abrufen
|
||||||
db.all(`SELECT DISTINCT week_start, week_end
|
db.all(`SELECT DISTINCT week_start, week_end
|
||||||
FROM weekly_timesheets
|
FROM weekly_timesheets
|
||||||
@@ -599,7 +608,11 @@ function registerUserRoutes(app) {
|
|||||||
|
|
||||||
// Wenn keine Wochen vorhanden
|
// Wenn keine Wochen vorhanden
|
||||||
if (!weeks || weeks.length === 0) {
|
if (!weeks || weeks.length === 0) {
|
||||||
return res.json({ weeks: [] });
|
return res.json({
|
||||||
|
weeks: [],
|
||||||
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
|
overtime_corrections: overtimeCorrections
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getCalendarWeek } = require('../helpers/utils');
|
const { getCalendarWeek } = require('../helpers/utils');
|
||||||
@@ -650,6 +663,7 @@ function registerUserRoutes(app) {
|
|||||||
const endDate = new Date(week.week_end);
|
const endDate = new Date(week.week_end);
|
||||||
let workdays = 0;
|
let workdays = 0;
|
||||||
let filledWorkdays = 0;
|
let filledWorkdays = 0;
|
||||||
|
const fullDayHoursForCheck = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
|
||||||
|
|
||||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
@@ -665,11 +679,13 @@ function registerUserRoutes(app) {
|
|||||||
if (entry) {
|
if (entry) {
|
||||||
const isFullDayVacation = entry.vacation_type === 'full';
|
const isFullDayVacation = entry.vacation_type === 'full';
|
||||||
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
||||||
|
const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHoursForCheck) < 0.01;
|
||||||
const hasStartAndEnd = entry.start_time && entry.end_time &&
|
const hasStartAndEnd = entry.start_time && entry.end_time &&
|
||||||
entry.start_time.toString().trim() !== '' &&
|
entry.start_time.toString().trim() !== '' &&
|
||||||
entry.end_time.toString().trim() !== '';
|
entry.end_time.toString().trim() !== '';
|
||||||
|
|
||||||
if (isFullDayVacation || isSick || hasStartAndEnd) {
|
if (isFullDayVacation || isSick || isFullDayOvertime || hasStartAndEnd) {
|
||||||
filledWorkdays++;
|
filledWorkdays++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,7 +696,11 @@ function registerUserRoutes(app) {
|
|||||||
if (filledWorkdays < workdays) {
|
if (filledWorkdays < workdays) {
|
||||||
processedWeeks++;
|
processedWeeks++;
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
res.json({ weeks: weekData });
|
res.json({
|
||||||
|
weeks: weekData,
|
||||||
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
|
overtime_corrections: overtimeCorrections
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -734,11 +754,10 @@ function registerUserRoutes(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sollstunden berechnen
|
// Sollstunden berechnen (Variante B: immer vertraglich)
|
||||||
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
||||||
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
||||||
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
|
||||||
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
|
||||||
|
|
||||||
// Kalenderwoche berechnen
|
// Kalenderwoche berechnen
|
||||||
const calendarWeek = getCalendarWeek(week.week_start);
|
const calendarWeek = getCalendarWeek(week.week_start);
|
||||||
@@ -753,7 +772,7 @@ function registerUserRoutes(app) {
|
|||||||
overtime_hours: parseFloat(weekOvertimeHours.toFixed(2)),
|
overtime_hours: parseFloat(weekOvertimeHours.toFixed(2)),
|
||||||
overtime_taken: parseFloat(weekOvertimeTaken.toFixed(2)),
|
overtime_taken: parseFloat(weekOvertimeTaken.toFixed(2)),
|
||||||
total_hours: parseFloat(weekTotalHoursWithVacation.toFixed(2)),
|
total_hours: parseFloat(weekTotalHoursWithVacation.toFixed(2)),
|
||||||
soll_stunden: parseFloat(adjustedSollStunden.toFixed(2)),
|
soll_stunden: parseFloat(sollStunden.toFixed(2)),
|
||||||
vacation_days: parseFloat(weekVacationDays.toFixed(1)),
|
vacation_days: parseFloat(weekVacationDays.toFixed(1)),
|
||||||
workdays: workdays
|
workdays: workdays
|
||||||
});
|
});
|
||||||
@@ -768,12 +787,18 @@ function registerUserRoutes(app) {
|
|||||||
if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week;
|
if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week;
|
||||||
return new Date(b.week_start) - new Date(a.week_start);
|
return new Date(b.week_start) - new Date(a.week_start);
|
||||||
});
|
});
|
||||||
res.json({ weeks: weekData, overtime_offset_hours: overtimeOffsetHours });
|
res.json({
|
||||||
|
weeks: weekData,
|
||||||
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
|
overtime_corrections: overtimeCorrections
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}); // getHolidaysForDateRange.then
|
}); // getHolidaysForDateRange.then
|
||||||
}); // db.all (allEntries)
|
}); // db.all (allEntries)
|
||||||
}); // weeks.forEach
|
}); // weeks.forEach
|
||||||
}); // db.all (weeks)
|
}); // db.all (weeks)
|
||||||
|
}
|
||||||
|
);
|
||||||
}); // db.get (user)
|
}); // db.get (user)
|
||||||
}); // db.get (options)
|
}); // db.get (options)
|
||||||
}); // app.get
|
}); // app.get
|
||||||
|
|||||||
@@ -105,8 +105,12 @@ function registerVerwaltungRoutes(app) {
|
|||||||
return new Date(b.week_start) - new Date(a.week_start);
|
return new Date(b.week_start) - new Date(a.week_start);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flag: Gibt es in irgendeiner Woche eine neue Version nach Download?
|
||||||
|
const hasNewVersionAfterDownload = sortedWeeks.some(w => w.has_new_version_after_download);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...employee,
|
...employee,
|
||||||
|
has_new_version_after_download: hasNewVersionAfterDownload,
|
||||||
weeks: sortedWeeks
|
weeks: sortedWeeks
|
||||||
};
|
};
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
@@ -132,6 +136,8 @@ function registerVerwaltungRoutes(app) {
|
|||||||
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
|
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const raw = req.body ? req.body.overtime_offset_hours : undefined;
|
const raw = req.body ? req.body.overtime_offset_hours : undefined;
|
||||||
|
const reasonRaw = req.body ? req.body.reason : undefined;
|
||||||
|
const reason = (reasonRaw === null || reasonRaw === undefined) ? '' : String(reasonRaw).trim();
|
||||||
|
|
||||||
// Leere Eingabe => 0
|
// Leere Eingabe => 0
|
||||||
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
||||||
@@ -139,12 +145,59 @@ function registerVerwaltungRoutes(app) {
|
|||||||
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
|
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
|
||||||
}
|
}
|
||||||
|
|
||||||
db.run('UPDATE users SET overtime_offset_hours = ? WHERE id = ?', [normalized, userId], (err) => {
|
// Neue Logik: Korrektur protokollieren + kumulativ addieren
|
||||||
if (err) {
|
// Feld in der Verwaltung soll nach dem Speichern immer auf 0 zurückgesetzt werden.
|
||||||
console.error('Fehler beim Speichern des Überstunden-Offsets:', err);
|
if (normalized === 0) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
|
return res.json({ success: true, overtime_offset_hours: 0 });
|
||||||
}
|
}
|
||||||
res.json({ success: true, overtime_offset_hours: normalized });
|
|
||||||
|
if (!reason) {
|
||||||
|
return res.status(400).json({ error: 'Bitte geben Sie einen Grund für die Korrektur an.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO overtime_corrections (user_id, correction_hours, reason, corrected_at)
|
||||||
|
VALUES (?, ?, ?, datetime('now'))`,
|
||||||
|
[userId, normalized, reason],
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Speichern der Überstunden-Korrektur:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
'UPDATE users SET overtime_offset_hours = COALESCE(overtime_offset_hours, 0) + ? WHERE id = ?',
|
||||||
|
[normalized, userId],
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Überstunden-Offsets:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run('COMMIT', (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Commit der Überstunden-Korrektur:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, overtime_offset_hours: 0 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,6 +221,26 @@ function registerVerwaltungRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Überstunden-Korrektur-Historie für einen User abrufen
|
||||||
|
app.get('/api/verwaltung/user/:id/overtime-corrections', requireVerwaltung, (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
|
||||||
|
db.all(
|
||||||
|
`SELECT correction_hours, corrected_at, reason
|
||||||
|
FROM overtime_corrections
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY corrected_at DESC`,
|
||||||
|
[userId],
|
||||||
|
(err, rows) => {
|
||||||
|
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
|
||||||
|
if (err) {
|
||||||
|
return res.json({ corrections: [] });
|
||||||
|
}
|
||||||
|
res.json({ corrections: rows || [] });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
||||||
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
@@ -524,12 +597,11 @@ function registerVerwaltungRoutes(app) {
|
|||||||
const weekOvertimeHours = totalHoursWithVacation - sollStunden;
|
const weekOvertimeHours = totalHoursWithVacation - sollStunden;
|
||||||
|
|
||||||
// Kumulative Überstunden: Summe aller Wochen bis zur aktuellen Woche
|
// Kumulative Überstunden: Summe aller Wochen bis zur aktuellen Woche
|
||||||
// cumulativeOvertimeHours enthält bereits alle vorherigen Wochen
|
|
||||||
const totalCumulativeOvertimeHours = cumulativeOvertimeHours + weekOvertimeHours;
|
const totalCumulativeOvertimeHours = cumulativeOvertimeHours + weekOvertimeHours;
|
||||||
const totalCumulativeOvertimeTaken = cumulativeOvertimeTaken + overtimeTaken;
|
const totalCumulativeOvertimeTaken = cumulativeOvertimeTaken + overtimeTaken;
|
||||||
|
|
||||||
// Verbleibende Überstunden = kumulative Überstunden - kumulative genommene Überstunden
|
// Variante B: Verbleibend = Summe Wochen-Überstunden („genommen“ nur Anzeige)
|
||||||
const remainingOvertime = totalCumulativeOvertimeHours - totalCumulativeOvertimeTaken;
|
const remainingOvertime = totalCumulativeOvertimeHours;
|
||||||
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
|
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
|
||||||
|
|
||||||
// Verbleibende Urlaubstage (berücksichtigt alle eingereichten Wochen, nicht nur die aktuelle)
|
// Verbleibende Urlaubstage (berücksichtigt alle eingereichten Wochen, nicht nur die aktuelle)
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ function getCurrentOvertimeForUser(userId, db, callback) {
|
|||||||
const endDate = new Date(week.week_end);
|
const endDate = new Date(week.week_end);
|
||||||
let workdays = 0;
|
let workdays = 0;
|
||||||
let filledWorkdays = 0;
|
let filledWorkdays = 0;
|
||||||
|
const fullDayHoursForCheck = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
|
||||||
|
|
||||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
@@ -108,26 +109,20 @@ function getCurrentOvertimeForUser(userId, db, callback) {
|
|||||||
if (entry) {
|
if (entry) {
|
||||||
const isFullDayVacation = entry.vacation_type === 'full';
|
const isFullDayVacation = entry.vacation_type === 'full';
|
||||||
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
const isSick = entry.sick_status === 1 || entry.sick_status === true;
|
||||||
|
const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
|
||||||
|
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHoursForCheck) < 0.01;
|
||||||
const hasStartAndEnd =
|
const hasStartAndEnd =
|
||||||
entry.start_time &&
|
entry.start_time &&
|
||||||
entry.end_time &&
|
entry.end_time &&
|
||||||
entry.start_time.toString().trim() !== '' &&
|
entry.start_time.toString().trim() !== '' &&
|
||||||
entry.end_time.toString().trim() !== '';
|
entry.end_time.toString().trim() !== '';
|
||||||
if (isFullDayVacation || isSick || hasStartAndEnd) {
|
if (isFullDayVacation || isSick || isFullDayOvertime || hasStartAndEnd) {
|
||||||
filledWorkdays++;
|
filledWorkdays++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filledWorkdays < workdays) {
|
|
||||||
processedWeeks++;
|
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
|
||||||
done(totalOvertimeHours - totalOvertimeTaken + overtimeOffsetHours);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
|
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
|
||||||
let weekTotalHours = 0;
|
let weekTotalHours = 0;
|
||||||
let weekOvertimeTaken = 0;
|
let weekOvertimeTaken = 0;
|
||||||
@@ -168,17 +163,18 @@ function getCurrentOvertimeForUser(userId, db, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
// Variante B: Soll immer vertraglich, Verbleibend ohne Abzug „genommen“
|
||||||
|
//const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
||||||
|
const sollStunden = wochenstunden;
|
||||||
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
||||||
const adjustedSollStunden = sollStunden - fullDayOvertimeDays * fullDayHours;
|
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
|
||||||
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
|
||||||
|
|
||||||
totalOvertimeHours += weekOvertimeHours;
|
totalOvertimeHours += weekOvertimeHours;
|
||||||
totalOvertimeTaken += weekOvertimeTaken;
|
totalOvertimeTaken += weekOvertimeTaken;
|
||||||
processedWeeks++;
|
processedWeeks++;
|
||||||
|
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
done(totalOvertimeHours - totalOvertimeTaken + overtimeOffsetHours);
|
done(totalOvertimeHours + overtimeOffsetHours);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ? '0h 0min (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) => {
|
||||||
@@ -229,7 +229,7 @@ function generatePDF(timesheetId, req, res) {
|
|||||||
const desc = entry[`activity${i}_desc`];
|
const desc = entry[`activity${i}_desc`];
|
||||||
const hours = entry[`activity${i}_hours`];
|
const hours = entry[`activity${i}_hours`];
|
||||||
const projectNumber = entry[`activity${i}_project_number`];
|
const projectNumber = entry[`activity${i}_project_number`];
|
||||||
if (desc && desc.trim() && hours > 0) {
|
if (desc && desc.trim()) {
|
||||||
activities.push({
|
activities.push({
|
||||||
desc: desc.trim(),
|
desc: desc.trim(),
|
||||||
hours: parseFloat(hours),
|
hours: parseFloat(hours),
|
||||||
@@ -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 ? '0h 0min (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) => {
|
||||||
@@ -500,7 +500,7 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
const desc = entry[`activity${i}_desc`];
|
const desc = entry[`activity${i}_desc`];
|
||||||
const hours = entry[`activity${i}_hours`];
|
const hours = entry[`activity${i}_hours`];
|
||||||
const projectNumber = entry[`activity${i}_project_number`];
|
const projectNumber = entry[`activity${i}_project_number`];
|
||||||
if (desc && desc.trim() && hours > 0) {
|
if (desc && desc.trim()) {
|
||||||
activities.push({
|
activities.push({
|
||||||
desc: desc.trim(),
|
desc: desc.trim(),
|
||||||
hours: parseFloat(hours),
|
hours: parseFloat(hours),
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { db } = require('../database');
|
|||||||
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils');
|
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils');
|
||||||
|
|
||||||
// Ping-Funktion für einen User
|
// Ping-Funktion für einen User
|
||||||
async function pingUserIP(userId, ip, currentDate, currentTime) {
|
async function pingUserIP(userId, ip, defaultBreakMinutes, currentDate, currentTime) {
|
||||||
try {
|
try {
|
||||||
const result = await ping.promise.probe(ip, {
|
const result = await ping.promise.probe(ip, {
|
||||||
timeout: 3,
|
timeout: 3,
|
||||||
@@ -31,6 +31,10 @@ async function pingUserIP(userId, ip, currentDate, currentTime) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userDefaultBreakMinutes = Number.isInteger(defaultBreakMinutes) && defaultBreakMinutes >= 0
|
||||||
|
? defaultBreakMinutes
|
||||||
|
: 30;
|
||||||
|
|
||||||
if (isReachable) {
|
if (isReachable) {
|
||||||
// IP ist erreichbar
|
// IP ist erreichbar
|
||||||
if (!pingStatus) {
|
if (!pingStatus) {
|
||||||
@@ -67,9 +71,9 @@ async function pingUserIP(userId, ip, currentDate, currentTime) {
|
|||||||
});
|
});
|
||||||
} else if (!entry) {
|
} else if (!entry) {
|
||||||
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
||||||
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at)
|
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||||
[userId, currentDate, currentTime], (err) => {
|
[userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err);
|
console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err);
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +165,7 @@ function setupPingService() {
|
|||||||
const currentTime = getCurrentTime();
|
const currentTime = getCurrentTime();
|
||||||
|
|
||||||
// Hole alle User mit IP-Adresse
|
// 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) {
|
if (err) {
|
||||||
console.error('Fehler beim Abrufen der User mit IP-Adressen:', err);
|
console.error('Fehler beim Abrufen der User mit IP-Adressen:', err);
|
||||||
return;
|
return;
|
||||||
@@ -173,7 +177,7 @@ function setupPingService() {
|
|||||||
|
|
||||||
// Ping alle User parallel
|
// Ping alle User parallel
|
||||||
users.forEach(user => {
|
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
|
}, 60000); // Jede Minute
|
||||||
|
|||||||
@@ -107,6 +107,11 @@
|
|||||||
<label for="urlaubstage">Urlaubstage</label>
|
<label for="urlaubstage">Urlaubstage</label>
|
||||||
<input type="number" id="urlaubstage" name="urlaubstage" step="0.5" min="0" placeholder="z.B. 25">
|
<input type="number" id="urlaubstage" name="urlaubstage" step="0.5" min="0" placeholder="z.B. 25">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="defaultBreakMinutes">Standard-Pausenzeit (Min)</label>
|
||||||
|
<input type="number" id="defaultBreakMinutes" name="default_break_minutes" min="0" step="15" placeholder="30">
|
||||||
|
<small style="color: #666; display: block; margin-top: 5px;">Vorbelegung Pausenzeit pro Tag (Min., mind. 0).</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Benutzer anlegen</button>
|
<button type="submit" class="btn btn-primary">Benutzer anlegen</button>
|
||||||
@@ -136,6 +141,7 @@
|
|||||||
<th>Wochenstunden</th>
|
<th>Wochenstunden</th>
|
||||||
<th>Arbeitstage pro Woche</th>
|
<th>Arbeitstage pro Woche</th>
|
||||||
<th>Urlaubstage</th>
|
<th>Urlaubstage</th>
|
||||||
|
<th>Standard-Pause (Min)</th>
|
||||||
<th>Erstellt am</th>
|
<th>Erstellt am</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -194,6 +200,10 @@
|
|||||||
<span class="user-field-display" data-field="urlaubstage"><%= u.urlaubstage || '-' %></span>
|
<span class="user-field-display" data-field="urlaubstage"><%= u.urlaubstage || '-' %></span>
|
||||||
<input type="number" step="0.5" class="user-field-edit" data-field="urlaubstage" data-user-id="<%= u.id %>" value="<%= u.urlaubstage || '' %>" style="display: none; width: 80px;">
|
<input type="number" step="0.5" class="user-field-edit" data-field="urlaubstage" data-user-id="<%= u.id %>" value="<%= u.urlaubstage || '' %>" style="display: none; width: 80px;">
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="user-field-display" data-field="default_break_minutes"><%= (u.default_break_minutes != null && u.default_break_minutes !== '') ? u.default_break_minutes : '-' %></span>
|
||||||
|
<input type="number" min="0" step="15" class="user-field-edit" data-field="default_break_minutes" data-user-id="<%= u.id %>" value="<%= (u.default_break_minutes != null && u.default_break_minutes !== '') ? u.default_break_minutes : '' %>" style="display: none; width: 80px;">
|
||||||
|
</td>
|
||||||
<td><%= new Date(u.created_at).toLocaleDateString('de-DE') %></td>
|
<td><%= new Date(u.created_at).toLocaleDateString('de-DE') %></td>
|
||||||
<td>
|
<td>
|
||||||
<button onclick="editUser(<%= u.id %>)" class="btn btn-primary btn-sm edit-user-btn" data-user-id="<%= u.id %>">Bearbeiten</button>
|
<button onclick="editUser(<%= u.id %>)" class="btn btn-primary btn-sm edit-user-btn" data-user-id="<%= u.id %>">Bearbeiten</button>
|
||||||
|
|||||||
@@ -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">0h 0min</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">0h 0min</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
|
||||||
|
|||||||
@@ -145,14 +145,23 @@
|
|||||||
<span class="summary-label">Davon genommen:</span>
|
<span class="summary-label">Davon genommen:</span>
|
||||||
<span class="summary-value" id="totalOvertimeTaken">-</span>
|
<span class="summary-value" id="totalOvertimeTaken">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Verbleibend:</span>
|
|
||||||
<span class="summary-value" id="remainingOvertime">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" id="offsetItem" style="display: none;">
|
<div class="summary-item" id="offsetItem" style="display: none;">
|
||||||
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
||||||
<span class="summary-value" id="overtimeOffset">-</span>
|
<span class="summary-value" id="overtimeOffset">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="correctionsSection" style="margin-top: 15px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #e0e0e0; display: none;">
|
||||||
|
<div id="correctionsHeader" class="collapsible-header" style="cursor: pointer; padding: 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span style="font-weight: 600; color: #2c3e50;">Korrekturen durch die Verwaltung</span>
|
||||||
|
<span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
|
||||||
|
</div>
|
||||||
|
<div id="correctionsContent" style="display: none; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff; padding: 10px 12px;">
|
||||||
|
<ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Verbleibend:</span>
|
||||||
|
<span class="summary-value" id="remainingOvertime">-</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading">Lade Daten...</div>
|
<div id="loading" class="loading">Lade Daten...</div>
|
||||||
@@ -178,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');
|
||||||
@@ -213,6 +223,27 @@
|
|||||||
return date.toLocaleDateString('de-DE');
|
return date.toLocaleDateString('de-DE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SQLite-Datetime (YYYY-MM-DD HH:MM:SS) robust parsen
|
||||||
|
function parseSqliteDatetime(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const s = String(value);
|
||||||
|
if (s.includes('T')) return new Date(s);
|
||||||
|
// SQLite datetime('now') liefert UTC ohne "T" / "Z"
|
||||||
|
return new Date(s.replace(' ', 'T') + 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatHoursMin aus format-hours.js (window.formatHoursMin)
|
||||||
|
|
||||||
|
let correctionsExpanded = false;
|
||||||
|
function toggleCorrectionsSection() {
|
||||||
|
const content = document.getElementById('correctionsContent');
|
||||||
|
const icon = document.getElementById('correctionsToggleIcon');
|
||||||
|
if (!content || !icon) return;
|
||||||
|
correctionsExpanded = !correctionsExpanded;
|
||||||
|
content.style.display = correctionsExpanded ? 'block' : 'none';
|
||||||
|
icon.style.transform = correctionsExpanded ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
|
||||||
// Überstunden-Daten laden
|
// Überstunden-Daten laden
|
||||||
async function loadOvertimeBreakdown() {
|
async function loadOvertimeBreakdown() {
|
||||||
const loadingEl = document.getElementById('loading');
|
const loadingEl = document.getElementById('loading');
|
||||||
@@ -243,23 +274,26 @@
|
|||||||
totalOvertimeTaken += week.overtime_taken;
|
totalOvertimeTaken += week.overtime_taken;
|
||||||
});
|
});
|
||||||
const overtimeOffset = data.overtime_offset_hours || 0;
|
const overtimeOffset = data.overtime_offset_hours || 0;
|
||||||
const remainingOvertime = totalOvertime - totalOvertimeTaken + overtimeOffset;
|
// Variante B: Verbleibend = Summe Wochen-Überstunden + Offset („genommen“ nur Anzeige)
|
||||||
|
const remainingOvertime = totalOvertime + overtimeOffset;
|
||||||
|
// Gesamt Überstunden = Verbleibend + Genommen (kumuliert inkl. bereits verbrauchter)
|
||||||
|
const displayTotalOvertime = remainingOvertime + totalOvertimeTaken;
|
||||||
|
|
||||||
// Zusammenfassung anzeigen
|
// Zusammenfassung anzeigen
|
||||||
const totalOvertimeEl = document.getElementById('totalOvertime');
|
const totalOvertimeEl = document.getElementById('totalOvertime');
|
||||||
totalOvertimeEl.textContent =
|
totalOvertimeEl.textContent =
|
||||||
(totalOvertime >= 0 ? '+' : '') + totalOvertime.toFixed(2) + ' h';
|
(displayTotalOvertime >= 0 ? '+' : '') + formatHoursMin(displayTotalOvertime);
|
||||||
totalOvertimeEl.className =
|
totalOvertimeEl.className =
|
||||||
'summary-value ' + (totalOvertime >= 0 ? 'overtime-positive' : 'overtime-negative');
|
'summary-value ' + (displayTotalOvertime >= 0 ? 'overtime-positive' : 'overtime-negative');
|
||||||
|
|
||||||
const totalOvertimeTakenEl = document.getElementById('totalOvertimeTaken');
|
const totalOvertimeTakenEl = document.getElementById('totalOvertimeTaken');
|
||||||
totalOvertimeTakenEl.textContent =
|
totalOvertimeTakenEl.textContent =
|
||||||
totalOvertimeTaken.toFixed(2) + ' h';
|
totalOvertimeTaken > 0 ? '-' + formatHoursMin(totalOvertimeTaken) : formatHoursMin(0);
|
||||||
totalOvertimeTakenEl.className = 'summary-value overtime-positive';
|
totalOvertimeTakenEl.className = 'summary-value overtime-negative';
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
@@ -267,13 +301,52 @@
|
|||||||
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 {
|
||||||
offsetItem.style.display = 'none';
|
offsetItem.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Korrekturen durch die Verwaltung anzeigen (Collapsible, nur wenn vorhanden)
|
||||||
|
const correctionsSectionEl = document.getElementById('correctionsSection');
|
||||||
|
const correctionsListEl = document.getElementById('correctionsList');
|
||||||
|
const correctionsHeaderEl = document.getElementById('correctionsHeader');
|
||||||
|
const correctionsContentEl = document.getElementById('correctionsContent');
|
||||||
|
const correctionsIconEl = document.getElementById('correctionsToggleIcon');
|
||||||
|
const corrections = Array.isArray(data.overtime_corrections) ? data.overtime_corrections : [];
|
||||||
|
|
||||||
|
if (correctionsSectionEl && correctionsListEl && correctionsHeaderEl && correctionsContentEl && correctionsIconEl && corrections.length > 0) {
|
||||||
|
correctionsSectionEl.style.display = 'block';
|
||||||
|
correctionsListEl.innerHTML = '';
|
||||||
|
|
||||||
|
corrections.forEach(c => {
|
||||||
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
|
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.innerHTML = reason
|
||||||
|
? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> – ${reason}`
|
||||||
|
: `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
|
||||||
|
correctionsListEl.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard: zugeklappt
|
||||||
|
correctionsExpanded = false;
|
||||||
|
correctionsContentEl.style.display = 'none';
|
||||||
|
correctionsIconEl.style.transform = 'rotate(0deg)';
|
||||||
|
|
||||||
|
// Click-Handler setzen (idempotent)
|
||||||
|
correctionsHeaderEl.onclick = toggleCorrectionsSection;
|
||||||
|
} else if (correctionsSectionEl) {
|
||||||
|
correctionsSectionEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
summaryBoxEl.style.display = 'block';
|
summaryBoxEl.style.display = 'block';
|
||||||
|
|
||||||
// Tabelle füllen
|
// Tabelle füllen
|
||||||
@@ -284,14 +357,16 @@
|
|||||||
const dateRange = formatDate(week.week_start) + ' - ' + formatDate(week.week_end);
|
const dateRange = formatDate(week.week_start) + ' - ' + formatDate(week.week_end);
|
||||||
const overtimeClass = week.overtime_hours >= 0 ? 'overtime-positive' : 'overtime-negative';
|
const overtimeClass = week.overtime_hours >= 0 ? 'overtime-positive' : 'overtime-negative';
|
||||||
const overtimeSign = week.overtime_hours >= 0 ? '+' : '';
|
const overtimeSign = week.overtime_hours >= 0 ? '+' : '';
|
||||||
|
const overtimeCell = Math.abs(week.overtime_hours) < 0.01 ? '-' : overtimeSign + formatHoursMin(week.overtime_hours);
|
||||||
|
const takenCell = week.overtime_taken > 0 ? '-' + formatHoursMin(week.overtime_taken) : '-';
|
||||||
|
|
||||||
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}">${overtimeCell}</td>
|
||||||
<td>${week.overtime_taken.toFixed(2)} h</td>
|
<td class="overtime-negative">${takenCell}</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);
|
||||||
|
|||||||
@@ -85,32 +85,48 @@
|
|||||||
<% if (employee.user.personalnummer) { %>
|
<% if (employee.user.personalnummer) { %>
|
||||||
<span style="margin-left: 10px; color: #666;">(Personalnummer: <%= employee.user.personalnummer %>)</span>
|
<span style="margin-left: 10px; color: #666;">(Personalnummer: <%= employee.user.personalnummer %>)</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (employee.has_new_version_after_download) { %>
|
||||||
|
<span class="employee-new-version-warning">ACHTUNG: Neue version eingereicht</span>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="employee-details" style="margin-top: 10px;">
|
<div class="employee-details" style="margin-top: 10px;">
|
||||||
<div style="display: inline-block; margin-right: 20px;">
|
<div style="display: inline-block; margin-right: 20px;">
|
||||||
<strong>Aktuelle Überstunden:</strong> <span class="current-overtime-value" data-user-id="<%= employee.user.id %>">-</span> Stunden
|
<strong>Aktuelle Überstunden:</strong> <span class="current-overtime-value" data-user-id="<%= employee.user.id %>">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-block; margin-right: 20px;">
|
<div style="display: inline-block; margin-right: 20px;">
|
||||||
<strong>Verbleibender Urlaub:</strong> <span class="remaining-vacation-value" data-user-id="<%= employee.user.id %>">-</span> Tage
|
<strong>Verbleibender Urlaub:</strong> <span class="remaining-vacation-value" data-user-id="<%= employee.user.id %>">-</span> Tage
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||||
<strong>Überstunden-Offset:</strong>
|
<strong>Überstunden-Korrektur:</strong>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
step="0.25"
|
|
||||||
class="overtime-offset-input"
|
class="overtime-offset-input"
|
||||||
data-user-id="<%= employee.user.id %>"
|
data-user-id="<%= employee.user.id %>"
|
||||||
value="<%= (employee.user.overtime_offset_hours !== undefined && employee.user.overtime_offset_hours !== null) ? employee.user.overtime_offset_hours : 0 %>"
|
value="0:00"
|
||||||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
placeholder="z. B. 1:30, 10:30 oder -0:45"
|
||||||
title="Manuelle Korrektur (positiv oder negativ) in Stunden" />
|
style="width: 110px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||||||
|
title="Korrektur in h:mm (z. B. 1:30, 10:30 oder -0:45). Nach dem Speichern wird das Feld auf 0:00 gesetzt." />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-success btn-sm save-overtime-offset-btn"
|
class="btn btn-success btn-sm save-overtime-offset-btn"
|
||||||
data-user-id="<%= employee.user.id %>"
|
data-user-id="<%= employee.user.id %>"
|
||||||
style="padding: 6px 10px; white-space: nowrap;"
|
style="padding: 6px 10px; white-space: nowrap;"
|
||||||
title="Überstunden-Offset speichern">
|
title="Überstunden-Korrektur speichern (addiert/abzieht und protokolliert)">
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm toggle-overtime-corrections-btn"
|
||||||
|
data-user-id="<%= employee.user.id %>"
|
||||||
|
style="padding: 6px 10px; white-space: nowrap;"
|
||||||
|
title="Korrektur-Historie anzeigen/ausblenden">
|
||||||
|
Historie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="overtime-corrections-container" data-user-id="<%= employee.user.id %>" style="display: none; margin: 10px; padding: 10px 12px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px;">
|
||||||
|
<div class="overtime-corrections-loading" style="color: #666; font-size: 13px;">Lade Korrekturen...</div>
|
||||||
|
<div class="overtime-corrections-empty" style="display: none; color: #999; font-size: 13px;">Keine Korrekturen vorhanden.</div>
|
||||||
|
<ul class="overtime-corrections-list" style="margin: 8px 0 0 0; padding-left: 18px; font-size: 13px;"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||||
<strong>Urlaubstage-Offset:</strong>
|
<strong>Urlaubstage-Offset:</strong>
|
||||||
@@ -302,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;
|
||||||
@@ -325,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) {
|
||||||
@@ -417,7 +434,7 @@
|
|||||||
el.textContent = '-';
|
el.textContent = '-';
|
||||||
el.style.color = '';
|
el.style.color = '';
|
||||||
} else {
|
} else {
|
||||||
el.textContent = value >= 0 ? '+' + value.toFixed(2) : value.toFixed(2);
|
el.textContent = (value >= 0 ? '+' : '') + formatHoursMin(value);
|
||||||
el.style.color = value >= 0 ? '#27ae60' : '#e74c3c';
|
el.style.color = value >= 0 ? '#27ae60' : '#e74c3c';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -489,6 +506,188 @@
|
|||||||
loadCurrentOvertime();
|
loadCurrentOvertime();
|
||||||
loadRemainingVacation();
|
loadRemainingVacation();
|
||||||
|
|
||||||
|
// Überstunden-Korrektur-Historie laden/anzeigen
|
||||||
|
function parseSqliteDatetime(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const s = String(value);
|
||||||
|
if (s.includes('T')) return new Date(s);
|
||||||
|
return new Date(s.replace(' ', 'T') + 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatHoursMin aus format-hours.js (window.formatHoursMin)
|
||||||
|
|
||||||
|
function showOvertimeCorrectionReasonModal(opts) {
|
||||||
|
const title = opts && opts.title ? String(opts.title) : 'Grund für die Korrektur';
|
||||||
|
const promptText = opts && opts.prompt ? String(opts.prompt) : 'Bitte geben Sie einen Grund an, warum die Korrektur vorgenommen wird.';
|
||||||
|
const onSubmit = opts && typeof opts.onSubmit === 'function' ? opts.onSubmit : null;
|
||||||
|
const onCancel = opts && typeof opts.onCancel === 'function' ? opts.onCancel : null;
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.style.cssText = `
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 520px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
padding: 18px 18px 14px 18px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px;">
|
||||||
|
<h3 style="margin: 0; font-size: 16px; color: #2c3e50;">${title}</h3>
|
||||||
|
<button type="button" data-action="close" class="btn btn-secondary btn-sm" style="padding: 6px 10px;">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="color:#666; font-size: 13px; margin-bottom: 10px;">${promptText}</div>
|
||||||
|
<textarea data-role="reason" rows="4" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-family: inherit; font-size: 13px; resize: vertical;" placeholder="Grund..."></textarea>
|
||||||
|
<div data-role="error" style="display:none; margin-top: 8px; color:#dc3545; font-size: 13px;"></div>
|
||||||
|
<div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 12px;">
|
||||||
|
<button type="button" data-action="cancel" class="btn btn-secondary">Abbrechen</button>
|
||||||
|
<button type="button" data-action="submit" class="btn btn-success">Speichern</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.appendChild(box);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const textarea = box.querySelector('textarea[data-role="reason"]');
|
||||||
|
const errorEl = box.querySelector('[data-role="error"]');
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
try {
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(msg) {
|
||||||
|
if (!errorEl) return;
|
||||||
|
errorEl.textContent = msg;
|
||||||
|
errorEl.style.display = msg ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const reason = textarea ? textarea.value.trim() : '';
|
||||||
|
if (!reason) {
|
||||||
|
setError('Bitte Grund angeben.');
|
||||||
|
if (textarea) textarea.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError('');
|
||||||
|
if (onSubmit) {
|
||||||
|
await onSubmit(reason);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) cancel();
|
||||||
|
});
|
||||||
|
box.querySelectorAll('button[data-action="close"], button[data-action="cancel"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', cancel);
|
||||||
|
});
|
||||||
|
const submitBtn = box.querySelector('button[data-action="submit"]');
|
||||||
|
if (submitBtn) submitBtn.addEventListener('click', submit);
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') cancel();
|
||||||
|
if ((e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOvertimeCorrectionsForUser(userId) {
|
||||||
|
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const loadingEl = container.querySelector('.overtime-corrections-loading');
|
||||||
|
const emptyEl = container.querySelector('.overtime-corrections-empty');
|
||||||
|
const listEl = container.querySelector('.overtime-corrections-list');
|
||||||
|
|
||||||
|
if (loadingEl) loadingEl.style.display = 'block';
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
if (listEl) listEl.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-corrections`);
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
const corrections = Array.isArray(data.corrections) ? data.corrections : [];
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(data.error || 'Fehler beim Laden der Korrekturen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (corrections.length === 0) {
|
||||||
|
if (emptyEl) emptyEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
corrections.forEach(c => {
|
||||||
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
|
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.innerHTML = reason
|
||||||
|
? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> – ${reason}`
|
||||||
|
: `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
|
||||||
|
if (listEl) listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Laden der Überstunden-Korrekturen:', e);
|
||||||
|
if (emptyEl) {
|
||||||
|
emptyEl.textContent = 'Fehler beim Laden der Korrekturen.';
|
||||||
|
emptyEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (loadingEl) loadingEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.toggle-overtime-corrections-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function() {
|
||||||
|
const userId = this.dataset.userId;
|
||||||
|
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const isOpen = container.style.display !== 'none' && container.style.display !== '';
|
||||||
|
if (isOpen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.display = 'block';
|
||||||
|
|
||||||
|
// Beim Öffnen immer neu laden (damit neue Korrekturen sofort sichtbar sind)
|
||||||
|
await loadOvertimeCorrectionsForUser(userId);
|
||||||
|
container.dataset.loaded = 'true';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Überstunden-Offset speichern
|
// Überstunden-Offset speichern
|
||||||
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async function() {
|
btn.addEventListener('click', async function() {
|
||||||
@@ -496,30 +695,59 @@
|
|||||||
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
|
if (this.dataset.modalOpen === 'true') return;
|
||||||
|
|
||||||
const originalText = this.textContent;
|
const originalText = this.textContent;
|
||||||
this.disabled = true;
|
|
||||||
this.textContent = '...';
|
|
||||||
|
|
||||||
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||||||
const raw = (input.value || '').trim();
|
const raw = (input.value || '').trim();
|
||||||
const value = raw === '' ? '' : Number(raw);
|
const value = (typeof parseHoursMin === 'function' ? parseHoursMin(raw) : null);
|
||||||
|
if (value === null && raw !== '') {
|
||||||
|
alert('Ungültiges Format. Bitte h:mm oder hh:mm eingeben (z. B. 1:30, 10:30 oder -0:45).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decimalValue = (value === null ? 0 : value);
|
||||||
|
|
||||||
|
// Wenn keine Korrektur (0), nichts tun außer UI auf 0:00 zu normalisieren
|
||||||
|
if (decimalValue === 0) {
|
||||||
|
input.value = (typeof decimalHoursToHhMm === 'function' ? decimalHoursToHhMm(0) : '0:00');
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataset.modalOpen = 'true';
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
// Modal: Grund ist Pflicht
|
||||||
|
showOvertimeCorrectionReasonModal({
|
||||||
|
title: 'Grund für die Überstunden-Korrektur',
|
||||||
|
prompt: `Korrektur: ${decimalValue >= 0 ? '+' : ''}${formatHoursMin(decimalValue)}`,
|
||||||
|
onCancel: () => {
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
|
this.disabled = false;
|
||||||
|
},
|
||||||
|
onSubmit: async (reason) => {
|
||||||
|
this.textContent = '...';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ overtime_offset_hours: value })
|
body: JSON.stringify({ overtime_offset_hours: decimalValue, reason })
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
alert(data.error || 'Fehler beim Speichern des Offsets');
|
alert(data.error || 'Fehler beim Speichern der Korrektur');
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
// Normalisiere Input auf h:mm (Backend gibt 0 zurück nach Speichern)
|
||||||
input.value = (data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null)
|
input.value = (typeof decimalHoursToHhMm === 'function')
|
||||||
? Number(data.overtime_offset_hours)
|
? decimalHoursToHhMm((data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null) ? Number(data.overtime_offset_hours) : 0)
|
||||||
: 0;
|
: '0:00';
|
||||||
|
|
||||||
// Stats für diesen User neu laden
|
// Stats für diesen User neu laden
|
||||||
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||||||
@@ -535,22 +763,29 @@
|
|||||||
});
|
});
|
||||||
loadCurrentOvertime();
|
loadCurrentOvertime();
|
||||||
|
|
||||||
|
// Historie (falls geöffnet) aktualisieren
|
||||||
|
const correctionsContainer = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (correctionsContainer && correctionsContainer.style.display !== 'none') {
|
||||||
|
await loadOvertimeCorrectionsForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
this.textContent = '✓';
|
this.textContent = '✓';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.textContent = originalText;
|
this.textContent = originalText;
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
}, 900);
|
}, 900);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Speichern des Offsets:', e);
|
console.error('Fehler beim Speichern der Korrektur:', e);
|
||||||
alert('Fehler beim Speichern des Offsets');
|
alert('Fehler beim Speichern der Korrektur');
|
||||||
} finally {
|
|
||||||
if (this.textContent === '...') {
|
|
||||||
this.textContent = originalText;
|
this.textContent = originalText;
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Urlaubstage-Offset speichern
|
// Urlaubstage-Offset speichern
|
||||||
document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => {
|
document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => {
|
||||||
|
|||||||
Reference in New Issue
Block a user