This commit is contained in:
2026-03-10 14:39:11 +01:00
parent bf578a8d87
commit d4a544164b
7 changed files with 217 additions and 51 deletions

View File

@@ -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 });
} }

View File

@@ -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;

View File

@@ -374,6 +374,52 @@ 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 {
@@ -603,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"
@@ -626,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">
@@ -1038,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 = '';
@@ -1047,6 +1110,10 @@ function handleOvertimeChange(dateStr, overtimeHours) {
hoursInput.value = ''; hoursInput.value = '';
saveEntry(hoursInput); saveEntry(hoursInput);
} }
if (minutesInput) {
minutesInput.value = '';
saveEntry(minutesInput);
}
} }
} }
@@ -1155,7 +1222,18 @@ async function saveEntry(input) {
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein // 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;
}
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);
@@ -1168,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)
}); });
} }
@@ -1337,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) {
@@ -1360,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) {
@@ -1371,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) {
@@ -1380,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) {
@@ -2118,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) {
@@ -2127,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) {
@@ -2137,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) {
@@ -2145,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) {
@@ -2161,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) {

View File

@@ -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,

View File

@@ -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

View File

@@ -145,15 +145,11 @@
<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; display: none;"> <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;"> <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 style="font-weight: 600; color: #2c3e50;">Korrekturen durch die Verwaltung</span>
<span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span> <span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
@@ -162,6 +158,10 @@
<ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul> <ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul>
</div> </div>
</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>
@@ -323,12 +323,16 @@
corrections.forEach(c => { corrections.forEach(c => {
const dt = parseSqliteDatetime(c.corrected_at); const dt = parseSqliteDatetime(c.corrected_at);
const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
const hoursText = formatHoursMin(c.correction_hours); const rawHours = Number(c.correction_hours) || 0;
const absHours = Math.abs(rawHours);
const signPrefix = rawHours >= 0 ? '+' : '-';
const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative';
const hoursDisplay = signPrefix + formatHoursMin(absHours);
const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = reason li.innerHTML = reason
? `Korrektur am ${dateText} ${hoursText} ${reason}` ? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> ${reason}`
: `Korrektur am ${dateText} ${hoursText}`; : `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
correctionsListEl.appendChild(li); correctionsListEl.appendChild(li);
}); });

View File

@@ -645,12 +645,16 @@
corrections.forEach(c => { corrections.forEach(c => {
const dt = parseSqliteDatetime(c.corrected_at); const dt = parseSqliteDatetime(c.corrected_at);
const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
const hoursText = formatHoursMin(c.correction_hours); const rawHours = Number(c.correction_hours) || 0;
const absHours = Math.abs(rawHours);
const signPrefix = rawHours >= 0 ? '+' : '-';
const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative';
const hoursDisplay = signPrefix + formatHoursMin(absHours);
const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = reason li.innerHTML = reason
? `Korrektur am ${dateText} ${hoursText} ${reason}` ? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> ${reason}`
: `Korrektur am ${dateText} ${hoursText}`; : `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
if (listEl) listEl.appendChild(li); if (listEl) listEl.appendChild(li);
}); });
} catch (e) { } catch (e) {