V1.1 Verschiedene Anpassungen

This commit is contained in:
2026-02-03 21:32:20 +01:00
parent 952c353118
commit 4be9a365b3
18 changed files with 1068 additions and 114 deletions

View File

@@ -5,6 +5,7 @@ let currentEntries = {};
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
let userWochenstunden = 0; // Wochenstunden des Users
let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal)
let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version
// Wochenend-Prozentsätze laden
async function loadWeekendPercentages() {
@@ -206,6 +207,21 @@ document.addEventListener('DOMContentLoaded', async function() {
} else {
console.error('Submit-Button nicht gefunden beim Initialisieren!');
}
// Event-Listener für PDF-Button
const viewPdfButton = document.getElementById('viewPdfBtn');
if (viewPdfButton) {
viewPdfButton.addEventListener('click', function(e) {
e.preventDefault();
if (!this.disabled && latestSubmittedTimesheetId) {
// Öffne PDF in neuem Tab
window.open(`/api/timesheet/pdf/${latestSubmittedTimesheetId}?inline=true`, '_blank');
}
});
console.log('PDF-Button Event-Listener gesetzt');
} else {
console.error('PDF-Button nicht gefunden beim Initialisieren!');
}
});
// Letzte Woche auf dem Server speichern
@@ -303,13 +319,18 @@ async function loadWeek() {
const parts = currentWeekStart.split('-');
const weekEndDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10) + 6);
const weekEnd = weekEndDate.getFullYear() + '-' + String(weekEndDate.getMonth() + 1).padStart(2, '0') + '-' + String(weekEndDate.getDate()).padStart(2, '0');
const [entriesResponse, holidaysResponse] = await Promise.all([
const [entriesResponse, holidaysResponse, latestSubmittedResponse] = await Promise.all([
fetch(`/api/timesheet/week/${currentWeekStart}`),
fetch(`/api/timesheet/holidays?week_start=${currentWeekStart}&week_end=${weekEnd}`)
fetch(`/api/timesheet/holidays?week_start=${currentWeekStart}&week_end=${weekEnd}`),
fetch(`/api/timesheet/latest-submitted/${currentWeekStart}`)
]);
const entries = await entriesResponse.json();
const holidaysData = await holidaysResponse.json();
const latestSubmittedData = await latestSubmittedResponse.json();
currentHolidayDates = new Set(holidaysData.dates || []);
// Speichere die neueste eingereichte Timesheet-ID
latestSubmittedTimesheetId = latestSubmittedData.timesheetId || null;
// Entries in Object umwandeln für schnellen Zugriff
currentEntries = {};
@@ -382,6 +403,9 @@ function renderWeek() {
const vacationType = entry.vacation_type || '';
const sickStatus = entry.sick_status || false;
const isHoliday = currentHolidayDates.has(dateStr);
const weekendTravel = entry.weekend_travel || false;
const appliedWeekendPercentage = entry.applied_weekend_percentage;
const isWeekend = (i >= 5); // Samstag (5) oder Sonntag (6)
// Tätigkeiten laden
const activities = [
@@ -413,10 +437,18 @@ function renderWeek() {
hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
} else {
hoursToAdd = hours || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
hoursToAdd = hours * (weekendPercentage / 100);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
let weekendPercentage = 100;
if (appliedWeekendPercentage !== null && appliedWeekendPercentage !== undefined) {
weekendPercentage = appliedWeekendPercentage;
} else {
weekendPercentage = getWeekendPercentage(date);
}
if (weekendPercentage >= 100) {
hoursToAdd = hours * (weekendPercentage / 100);
}
}
}
totalHours += hoursToAdd;
@@ -537,20 +569,17 @@ function renderWeek() {
</div>
</div>
<div class="sick-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSickStatus('${dateStr}')" style="margin-right: 5px;">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSickStatus('${dateStr}')" style="margin-right: 5px; ${sickStatus ? 'background-color: #e74c3c; color: white;' : ''}">
Krank
</button>
<div id="sick-checkbox-${dateStr}" style="display: ${sickStatus ? 'inline-block' : 'none'};">
<input type="checkbox"
data-date="${dateStr}"
data-field="sick_status"
${sickStatus ? 'checked' : ''}
${disabled}
onchange="saveEntry(this); updateOvertimeDisplay();"
style="margin-left: 5px;"
class="sick-checkbox">
</div>
</div>
${isWeekend ? `
<div class="travel-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleWeekendTravel('${dateStr}')" style="margin-right: 5px; ${weekendTravel ? 'background-color: #28a745; color: white;' : ''}">
Reise
</button>
</div>
` : ''}
</div>
</td>
</tr>
@@ -671,8 +700,8 @@ function updateOvertimeDisplay() {
// Prüfe Urlaub-Status und Krank-Status
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || '');
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false);
// Für sick_status: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
const sickStatus = currentEntries[dateStr]?.sick_status || false;
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
@@ -703,20 +732,46 @@ function updateOvertimeDisplay() {
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
const dayOfWeek = date.getDay();
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
// Für weekend_travel: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
const weekendTravel = currentEntries[dateStr]?.weekend_travel || false;
let adjustedHours = hours;
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
adjustedHours = hours * (weekendPercentage / 100);
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
let weekendPercentage = 100;
const entry = currentEntries[dateStr] || {};
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
weekendPercentage = entry.applied_weekend_percentage;
} else {
weekendPercentage = getWeekendPercentage(date);
}
if (weekendPercentage >= 100) {
adjustedHours = hours * (weekendPercentage / 100);
}
}
totalHours += adjustedHours;
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
// Fallback auf gespeicherte Werte
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus) {
hours = hours * (weekendPercentage / 100);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
const dayOfWeek = date.getDay();
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
// Für weekend_travel: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
const weekendTravel = currentEntries[dateStr]?.weekend_travel || false;
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus) {
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
const entry = currentEntries[dateStr] || {};
let weekendPercentage = 100;
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
weekendPercentage = entry.applied_weekend_percentage;
} else {
weekendPercentage = getWeekendPercentage(date);
}
if (weekendPercentage >= 100) {
hours = hours * (weekendPercentage / 100);
}
}
totalHours += hours;
}
@@ -759,20 +814,46 @@ function updateOvertimeDisplay() {
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
const dayOfWeek = date.getDay();
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
const travelCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="weekend_travel"]`);
const weekendTravel = travelCheckbox ? travelCheckbox.checked : (currentEntries[dateStr]?.weekend_travel || false);
let adjustedHours = hours;
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
adjustedHours = hours * (weekendPercentage / 100);
if (isWeekend && weekendTravel && hours > 0 && !isFullDayOvertime) {
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
const entry = currentEntries[dateStr] || {};
let weekendPercentage = 100;
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
weekendPercentage = entry.applied_weekend_percentage;
} else {
weekendPercentage = getWeekendPercentage(date);
}
if (weekendPercentage >= 100) {
adjustedHours = hours * (weekendPercentage / 100);
}
}
totalHours += adjustedHours;
} else if (currentEntries[dateStr]?.total_hours) {
// Fallback auf gespeicherte Werte
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
hours = hours * (weekendPercentage / 100);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
const dayOfWeek = date.getDay();
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
const travelCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="weekend_travel"]`);
const weekendTravel = travelCheckbox ? travelCheckbox.checked : (currentEntries[dateStr]?.weekend_travel || false);
if (isWeekend && weekendTravel && hours > 0 && !isFullDayOvertime) {
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
const entry = currentEntries[dateStr] || {};
let weekendPercentage = 100;
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
weekendPercentage = entry.applied_weekend_percentage;
} else {
weekendPercentage = getWeekendPercentage(date);
}
if (weekendPercentage >= 100) {
hours = hours * (weekendPercentage / 100);
}
}
totalHours += hours;
}
@@ -893,6 +974,27 @@ function handleOvertimeChange(dateStr, overtimeHours) {
}
}
// Berechnet die gesetzlich erforderliche Mindestpause basierend auf der Arbeitszeit
function calculateRequiredBreakMinutes(startTime, endTime) {
if (!startTime || !endTime) return null;
// Berechne Arbeitszeit in Stunden
const [startHours, startMinutes] = startTime.split(':').map(Number);
const [endHours, endMinutes] = endTime.split(':').map(Number);
const startTotalMinutes = startHours * 60 + startMinutes;
const endTotalMinutes = endHours * 60 + endMinutes;
const workMinutes = endTotalMinutes - startTotalMinutes;
const workHours = workMinutes / 60;
// Gesetzliche Mindestpause bestimmen
if (workHours > 9) {
return 45; // Mehr als 9 Stunden: 45 Minuten
} else if (workHours >= 6) {
return 30; // 6 bis 9 Stunden: 30 Minuten
}
return 0; // Weniger als 6 Stunden: keine gesetzliche Pause erforderlich
}
// Eintrag speichern
async function saveEntry(input) {
const date = input.dataset.date;
@@ -936,11 +1038,27 @@ async function saveEntry(input) {
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
const start_time = actualStartTime;
const end_time = actualEndTime;
const break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
// Automatische Vorbelegung der Pausenzeiten basierend auf gesetzlichen Vorgaben
// Wird ausgelöst, wenn start_time oder end_time geändert werden
if ((input.dataset.field === 'start_time' || input.dataset.field === 'end_time') && start_time && end_time) {
const requiredBreakMinutes = calculateRequiredBreakMinutes(start_time, end_time);
if (requiredBreakMinutes !== null && requiredBreakMinutes > break_minutes) {
// Setze den höheren Wert (gesetzliche Mindestpause)
break_minutes = requiredBreakMinutes;
// Aktualisiere das Input-Feld im DOM
if (breakInput) {
breakInput.value = break_minutes;
}
}
}
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null);
const sick_status = sickCheckbox ? (sickCheckbox.checked ? true : false) : (currentEntries[date].sick_status || false);
// Für sick_status und weekend_travel: Wert aus currentEntries lesen (da keine Checkboxen mehr vorhanden)
const sick_status = (input.dataset.field === 'sick_status') ? (value === 'true' || value === true || value === '1' || value === 1) : (currentEntries[date].sick_status || false);
const weekend_travel = (input.dataset.field === 'weekend_travel') ? (value === 'true' || value === true || value === '1' || value === 1) : (currentEntries[date].weekend_travel || false);
// Activity-Felder aus DOM lesen
const activities = [];
@@ -964,6 +1082,7 @@ async function saveEntry(input) {
currentEntries[date].vacation_type = vacation_type;
currentEntries[date].overtime_taken_hours = overtime_taken_hours;
currentEntries[date].sick_status = sick_status;
currentEntries[date].weekend_travel = weekend_travel;
for (let i = 1; i <= 5; i++) {
currentEntries[date][`activity${i}_desc`] = activities[i-1].desc;
currentEntries[date][`activity${i}_hours`] = activities[i-1].hours;
@@ -1002,7 +1121,8 @@ async function saveEntry(input) {
activity5_project_number: activities[4].projectNumber,
overtime_taken_hours: overtime_taken_hours,
vacation_type: vacation_type,
sick_status: sick_status
sick_status: sick_status,
weekend_travel: weekend_travel
})
});
@@ -1063,6 +1183,9 @@ function checkWeekComplete() {
date.setDate(date.getDate() + i);
const dateStr = formatDate(date);
// Prüfe ob Feiertag
const isHoliday = currentHolidayDates.has(dateStr);
// Prüfe Urlaub-Status und Krank-Status
const entry = currentEntries[dateStr] || {};
const vacationType = entry.vacation_type;
@@ -1071,9 +1194,9 @@ function checkWeekComplete() {
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
// Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
if (vacationValue === 'full' || sickStatus) {
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
// Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
if (isHoliday || vacationValue === 'full' || sickStatus) {
continue; // Tag ist ausgefüllt (Feiertag, ganzer Tag Urlaub oder Krank)
}
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt)
@@ -1132,6 +1255,18 @@ function checkWeekComplete() {
console.log(`Submit-Button Status: disabled=${submitButton.disabled}, allWeekdaysFilled=${allWeekdaysFilled}, weekIsSubmitted=${weekIsSubmitted}`);
}
// PDF-Button Status aktualisieren
const viewPdfButton = document.getElementById('viewPdfBtn');
if (viewPdfButton) {
// Button aktivieren wenn eine eingereichte Version existiert
viewPdfButton.disabled = !latestSubmittedTimesheetId;
if (latestSubmittedTimesheetId) {
viewPdfButton.title = 'Letzte eingereichte Version anzeigen';
} else {
viewPdfButton.title = 'Keine eingereichte Version verfügbar';
}
}
}
// Globaler Handler für onclick-Attribut (im HTML)
@@ -1185,6 +1320,9 @@ async function submitWeek() {
const weekday = getWeekday(dateStr);
const dateDisplay = formatDateDE(dateStr);
// Prüfe ob Feiertag
const isHoliday = currentHolidayDates.has(dateStr);
// Prüfe Urlaub-Status und Krank-Status
const entry = currentEntries[dateStr] || {};
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
@@ -1192,9 +1330,9 @@ async function submitWeek() {
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
// Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
if (vacationValue === 'full' || sickStatus) {
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
// Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
if (isHoliday || vacationValue === 'full' || sickStatus) {
continue; // Tag ist ausgefüllt (Feiertag, ganzer Tag Urlaub oder Krank)
}
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig)
@@ -1474,37 +1612,69 @@ function toggleVacationSelect(dateStr) {
}
}
// Krank-Status ein-/ausblenden
function toggleSickStatus(dateStr) {
const checkboxDiv = document.getElementById(`sick-checkbox-${dateStr}`);
if (checkboxDiv) {
if (checkboxDiv.style.display === 'none' || !checkboxDiv.style.display) {
checkboxDiv.style.display = 'inline-block';
const checkbox = checkboxDiv.querySelector('input[type="checkbox"]');
if (checkbox) {
// Prüfe aktuellen Status aus currentEntries
const currentSickStatus = currentEntries[dateStr]?.sick_status || false;
checkbox.checked = currentSickStatus || true; // Wenn nicht gesetzt, auf true setzen
checkbox.focus();
// Sofort speichern wenn aktiviert
if (!currentSickStatus) {
saveEntry(checkbox);
}
}
} else {
// Wert löschen wenn ausgeblendet
const checkbox = checkboxDiv.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = false;
// Speichern
if (currentEntries[dateStr]) {
currentEntries[dateStr].sick_status = false;
saveEntry(checkbox);
}
}
checkboxDiv.style.display = 'none';
}
// Wochenend-Reise-Status umschalten
function toggleWeekendTravel(dateStr) {
const button = document.querySelector(`button[onclick="toggleWeekendTravel('${dateStr}')"]`);
if (!button) return;
// Aktuellen Status aus currentEntries lesen
const currentTravelStatus = currentEntries[dateStr]?.weekend_travel || false;
const newStatus = !currentTravelStatus;
// Status in currentEntries aktualisieren
if (!currentEntries[dateStr]) {
currentEntries[dateStr] = { date: dateStr };
}
currentEntries[dateStr].weekend_travel = newStatus;
// Button-Stil aktualisieren
if (newStatus) {
button.style.backgroundColor = '#28a745';
button.style.color = 'white';
} else {
button.style.backgroundColor = '';
button.style.color = '';
}
// Speichere den Wert (erstellen ein temporäres Input-Element für saveEntry)
const tempInput = document.createElement('input');
tempInput.dataset.date = dateStr;
tempInput.dataset.field = 'weekend_travel';
tempInput.value = newStatus;
saveEntry(tempInput);
updateOvertimeDisplay();
}
function toggleSickStatus(dateStr) {
const button = document.querySelector(`button[onclick="toggleSickStatus('${dateStr}')"]`);
if (!button) return;
// Aktuellen Status aus currentEntries lesen
const currentSickStatus = currentEntries[dateStr]?.sick_status || false;
const newStatus = !currentSickStatus;
// Status in currentEntries aktualisieren
if (!currentEntries[dateStr]) {
currentEntries[dateStr] = { date: dateStr };
}
currentEntries[dateStr].sick_status = newStatus;
// Button-Stil aktualisieren
if (newStatus) {
button.style.backgroundColor = '#e74c3c';
button.style.color = 'white';
} else {
button.style.backgroundColor = '';
button.style.color = '';
}
// Speichere den Wert (erstellen ein temporäres Input-Element für saveEntry)
const tempInput = document.createElement('input');
tempInput.dataset.date = dateStr;
tempInput.dataset.field = 'sick_status';
tempInput.value = newStatus;
saveEntry(tempInput);
updateOvertimeDisplay();
}
// Ping-IP laden