`;
}
// Werktage (Montag bis Freitag) rendern
for (let i = 0; i < 5; i++) {
html += renderDay(i);
}
// Wochenende (Samstag und Sonntag) in zusammenklappbarer Sektion
html += `
Wochenende
▼
`;
// Samstag und Sonntag rendern
for (let i = 5; i < 7; i++) {
html += renderDay(i);
}
html += `
`;
html += `
`;
document.getElementById('timesheetTable').innerHTML = html;
document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h';
// Überstunden-Berechnung (startDate und endDate sind bereits oben deklariert)
// Anzahl Werktage berechnen (Montag-Freitag)
let workdays = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) { // Montag bis Freitag
workdays++;
}
}
// Sollstunden berechnen
// Die Sollstunden für eine Woche sind immer die Wochenstunden, unabhängig von den Arbeitstagen
// Die Arbeitstage pro Woche bestimmen nur die Stunden pro Tag (für Urlaub/Krank), nicht die Wochenstunden
const sollStunden = userWochenstunden || 0;
// Urlaubsstunden berechnen (Urlaub zählt als normale Arbeitszeit)
let vacationHours = 0;
const fullDayHours = getFullDayHours();
Object.values(currentEntries).forEach(e => {
if (e.vacation_type === 'full') {
vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden
} else if (e.vacation_type === 'half') {
vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden
}
});
// Überstunden berechnen
let overtimeTaken = 0;
Object.values(currentEntries).forEach(e => {
if (e.overtime_taken_hours) {
overtimeTaken += parseFloat(e.overtime_taken_hours) || 0;
}
});
// Überstunden-Berechnung aufrufen
updateOvertimeDisplay();
// Nach dem Rendern die vollständige Validierung durchführen
// Dies prüft auch direkt die Input-Felder im DOM
checkWeekComplete();
}
// Überstunden-Anzeige aktualisieren (wird bei jeder Änderung aufgerufen)
function updateOvertimeDisplay() {
const startDate = new Date(currentWeekStart);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
// Anzahl Werktage berechnen (Montag-Freitag)
let workdays = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) { // Montag bis Freitag
workdays++;
}
}
// Sollstunden berechnen
// Die Sollstunden für eine Woche sind immer die Wochenstunden, unabhängig von den Arbeitstagen
// Die Arbeitstage pro Woche bestimmen nur die Stunden pro Tag (für Urlaub/Krank), nicht die Wochenstunden
const sollStunden = userWochenstunden || 0;
// Gesamtstunden berechnen - direkt aus DOM-Elementen lesen für Echtzeit-Aktualisierung
let totalHours = 0;
let vacationHours = 0;
const fullDayHours = getFullDayHours(); // Verwende die Hilfsfunktion statt manueller Berechnung
let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden (wie im Backend)
const startDateObj = new Date(startDate);
for (let i = 0; i < 7; i++) {
const date = new Date(startDateObj);
date.setDate(date.getDate() + i);
const dateStr = formatDate(date);
// 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 || '');
// 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"]`);
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (currentEntries[dateStr]?.overtime_taken_hours ? parseFloat(currentEntries[dateStr].overtime_taken_hours) : 0);
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
// Zähle volle Überstundentage (wie im Backend)
if (isFullDayOvertime) {
fullDayOvertimeDays++;
}
// Wenn Urlaub oder Krank, zähle nur diese Stunden (nicht zusätzlich Arbeitsstunden)
if (vacationType === 'full') {
vacationHours += fullDayHours; // Ganzer Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) Stunden
} else if (vacationType === 'half') {
vacationHours += fullDayHours / 2; // Halber Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
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"]`);
const startTime = startInput ? startInput.value : '';
const endTime = endInput ? endInput.value : '';
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
if (startTime && endTime && !isFullDayOvertime) {
const start = new Date(`2000-01-01T${startTime}`);
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
// 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 (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 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;
}
} else if (sickStatus) {
totalHours += fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden
} else if (currentHolidayDates.has(dateStr)) {
// Feiertag: (Wochenarbeitszeit / Arbeitstage) Basis + gearbeitete Stunden (jede Stunde = Überstunde)
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 startTime = startInput ? startInput.value : '';
const endTime = endInput ? endInput.value : '';
let worked = 0;
if (startTime && endTime) {
const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`);
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
const start = new Date(`2000-01-01T${startTime}`);
const end = new Date(`2000-01-01T${endTime}`);
worked = (end - start) / (1000 * 60 * 60) - (breakMinutes / 60);
} else if (currentEntries[dateStr]?.total_hours) {
worked = parseFloat(currentEntries[dateStr].total_hours) || 0;
}
totalHours += fullDayHours + worked; // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden)
} else {
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden
if (isFullDayOvertime) {
// Tag zählt als 0 Stunden (Überstunden werden separat abgezogen)
// totalHours bleibt unverändert (0 Stunden für diesen Tag)
} else {
// Berechne Stunden direkt aus Start-/Endzeit und Pause
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"]`);
const startTime = startInput ? startInput.value : '';
const endTime = endInput ? endInput.value : '';
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
if (startTime && endTime) {
const start = new Date(`2000-01-01T${startTime}`);
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
// 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 (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 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;
}
}
}
}
// Genommene Überstunden berechnen - direkt aus DOM lesen
let overtimeTaken = 0;
const startDateObj3 = new Date(startDate);
for (let i = 0; i < 7; i++) {
const date = new Date(startDateObj3);
date.setDate(date.getDate() + i);
const dateStr = formatDate(date);
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
if (overtimeInput && overtimeInput.value) {
overtimeTaken += parseFloat(overtimeInput.value) || 0;
} else if (currentEntries[dateStr]?.overtime_taken_hours) {
overtimeTaken += parseFloat(currentEntries[dateStr].overtime_taken_hours) || 0;
}
}
// Überstunden berechnen (wie im Backend: mit adjustedSollStunden)
// totalHours enthält bereits Feiertagsstunden (8h oder gearbeitete Stunden) aus dem Feiertag-Zweig oben
const totalHoursWithVacation = totalHours + vacationHours;
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
// overtimeHours = Überstunden diese Woche (wie im Backend berechnet)
const overtimeHours = totalHoursWithVacation - adjustedSollStunden;
// Überstunden-Anzeige aktualisieren
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
const overtimeHoursSpan = document.getElementById('overtimeHours');
if (overtimeSummaryItem && overtimeHoursSpan) {
overtimeSummaryItem.style.display = 'block';
const sign = overtimeHours >= 0 ? '+' : '';
overtimeHoursSpan.textContent = `${sign}${overtimeHours.toFixed(2)} h`;
overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c';
}
// Gesamtstunden-Anzeige aktualisieren
const totalHoursElement = document.getElementById('totalHours');
if (totalHoursElement) {
totalHoursElement.textContent = totalHoursWithVacation.toFixed(2) + ' h';
}
}
// Überstunden-Änderung verarbeiten
function handleOvertimeChange(dateStr, overtimeHours) {
if (!userWochenstunden || userWochenstunden <= 0) {
console.warn('Wochenstunden nicht verfügbar, kann Überstunden-Logik nicht anwenden');
return;
}
const fullDayHours = getFullDayHours();
const overtimeValue = parseFloat(overtimeHours) || 0;
// Entferne "Überstunden" aus Activity-Feldern falls vorhanden
// (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit)
for (let i = 1; i <= 5; i++) {
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"]`);
if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') {
descInput.value = '';
saveEntry(descInput);
if (hoursInput) {
hoursInput.value = '';
saveEntry(hoursInput);
}
}
}
// Prüfe ob ganzer Tag Überstunden
if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
// Ganzer Tag Überstunden - leere Start- und End-Zeit
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
if (startInput) {
startInput.value = '';
saveEntry(startInput);
}
if (endInput) {
endInput.value = '';
saveEntry(endInput);
}
}
// Bei weniger als ganzer Tag oder keine Überstunden: keine weiteren Aktionen
// Überstunden werden nur im PDF als Information angezeigt
}
// 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;
const field = input.dataset.field;
const value = input.value;
// Entferne Fehlermarkierung wenn Feld ausgefüllt wird
if (input.classList.contains('missing-field')) {
input.classList.remove('missing-field');
input.style.borderColor = '';
input.style.backgroundColor = '';
}
// Aktualisiere currentEntries
if (!currentEntries[date]) {
currentEntries[date] = { date };
}
currentEntries[date][field] = value;
// Lese alle aktuellen Werte direkt aus dem DOM, nicht nur aus currentEntries
// Das stellt sicher, dass auch Werte gespeichert werden, die noch nicht in currentEntries sind
// WICHTIG: Wenn das aktuelle Input-Element das Feld ist, das wir suchen, verwende direkt dessen Wert
const startInput = document.querySelector(`input[data-date="${date}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${date}"][data-field="end_time"]`);
const breakInput = document.querySelector(`input[data-date="${date}"][data-field="break_minutes"]`);
const notesInput = document.querySelector(`textarea[data-date="${date}"][data-field="notes"]`);
const vacationSelect = document.querySelector(`select[data-date="${date}"][data-field="vacation_type"]`);
const overtimeInput = document.querySelector(`input[data-date="${date}"][data-field="overtime_taken_hours"]`);
const sickCheckbox = document.querySelector(`input[data-date="${date}"][data-field="sick_status"]`);
// Wenn das aktuelle Input-Element das gesuchte Feld ist, verwende dessen Wert direkt
// Das stellt sicher, dass der Wert auch bei oninput/onchange sofort verfügbar ist
const actualStartTime = (input.dataset.field === 'start_time' && input.value) ? input.value :
(startInput && startInput.value && startInput.value.trim() !== '') ? startInput.value :
(currentEntries[date].start_time || null);
const actualEndTime = (input.dataset.field === 'end_time' && input.value) ? input.value :
(endInput && endInput.value && endInput.value.trim() !== '') ? endInput.value :
(currentEntries[date].end_time || null);
// Aktuelle Werte aus DOM lesen (falls vorhanden), sonst aus currentEntries
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
const start_time = actualStartTime;
const end_time = actualEndTime;
let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
// Automatische Vorbelegung der Pausenzeiten basierend auf gesetzlichen Vorgaben
// Wird ausgelöst, wenn start_time oder end_time geändert werden
if ((input.dataset.field === 'start_time' || input.dataset.field === 'end_time') && start_time && end_time) {
const requiredBreakMinutes = calculateRequiredBreakMinutes(start_time, end_time);
if (requiredBreakMinutes !== null && requiredBreakMinutes > break_minutes) {
// Setze den höheren Wert (gesetzliche Mindestpause)
break_minutes = requiredBreakMinutes;
// Aktualisiere das Input-Feld im DOM
if (breakInput) {
breakInput.value = break_minutes;
}
}
}
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null);
// 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 = [];
for (let i = 1; i <= 5; i++) {
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
activities.push({
desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null),
hours: hoursInput ? (parseFloat(hoursInput.value) || 0) : (parseFloat(currentEntries[date][`activity${i}_hours`]) || 0),
projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null)
});
}
// Aktualisiere currentEntries mit den DOM-Werten
currentEntries[date].start_time = start_time;
currentEntries[date].end_time = end_time;
currentEntries[date].break_minutes = break_minutes;
currentEntries[date].notes = notes;
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;
currentEntries[date][`activity${i}_project_number`] = activities[i-1].projectNumber;
}
// SOFORTIGE DOM-UPDATES wenn vacation_type geändert wurde
if (input.dataset.field === 'vacation_type') {
const isFullDayVacation = vacation_type === 'full';
const isHalfDayVacation = vacation_type === 'half';
const fullDayHours = getFullDayHours();
const entry = currentEntries[date];
const isHoliday = currentHolidayDates.has(date);
const isSick = entry.sick_status || false;
const hours = entry.total_hours || 0;
// 1. Datum-Zelle: "(Urlaub - ganzer Tag)" in grün hinzufügen/entfernen
// Suche die Datum-Zelle über verschiedene Wege
let dateCell = null;
// Versuche zuerst über das Select-Element selbst
if (input.tagName === 'SELECT' && input.dataset.date === date) {
const selectRow = input.closest('tr');
if (selectRow) {
// Das Select ist in der activities-row, suche die vorherige Zeile
const prevRow = selectRow.previousElementSibling;
if (prevRow) {
dateCell = prevRow.querySelector('td:nth-child(2)');
}
}
}
// Fallback: Suche über start_time Input
if (!dateCell) {
const startInput = document.querySelector(`input[data-date="${date}"][data-field="start_time"]`);
if (startInput) {
const row = startInput.closest('tr');
if (row) {
dateCell = row.querySelector('td:nth-child(2)'); // Zweite Spalte ist das Datum
}
}
}
// Fallback: Suche direkt über alle Tabellenzeilen
if (!dateCell) {
const allRows = document.querySelectorAll('#timesheetTable tr, table tr');
for (let row of allRows) {
const testInput = row.querySelector(`input[data-date="${date}"][data-field="start_time"]`);
if (testInput) {
dateCell = row.querySelector('td:nth-child(2)');
break;
}
}
}
if (dateCell) {
let dateText = dateCell.innerHTML;
const vacationSpan = '(Urlaub - ganzer Tag)';
if (isFullDayVacation) {
// Entferne zuerst alle Urlaub-Spans falls vorhanden (mit verschiedenen möglichen Formaten)
dateText = dateText.replace(/\s*]*>\(Urlaub - ganzer Tag\)<\/span>/gi, '');
// Entferne auch "(Krank)" falls vorhanden
dateText = dateText.replace(/\s*]*>\(Krank\)<\/span>/gi, '');
// Füge "(Urlaub - ganzer Tag)" hinzu, wenn noch nicht vorhanden
if (!dateText.includes('(Urlaub - ganzer Tag)')) {
dateText += ' ' + vacationSpan;
}
dateCell.innerHTML = dateText;
} else {
// Entferne "(Urlaub - ganzer Tag)" Span (mit verschiedenen möglichen Formaten)
dateText = dateText.replace(/\s*]*>\(Urlaub - ganzer Tag\)<\/span>/gi, '');
dateCell.innerHTML = dateText;
}
}
// 2. Stunden-Anzeige sofort aktualisieren
const hoursElement = document.getElementById(`hours_${date}`);
if (hoursElement) {
if (isFullDayVacation) {
// Ganzer Tag Urlaub: Zeige fullDayHours mit "(Urlaub)" Label
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)';
currentEntries[date].total_hours = fullDayHours;
} else if (isHalfDayVacation) {
// Halber Tag Urlaub: Berechne Stunden aus Start/Ende falls vorhanden
const startInput = document.querySelector(`input[data-date="${date}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${date}"][data-field="end_time"]`);
const breakInput = document.querySelector(`input[data-date="${date}"][data-field="break_minutes"]`);
const halfHours = fullDayHours / 2;
let workHours = 0;
if (startInput && endInput && startInput.value && endInput.value) {
const start = new Date(`2000-01-01T${startInput.value}`);
const end = new Date(`2000-01-01T${endInput.value}`);
const diffMs = end - start;
const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0;
workHours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
}
const totalHours = halfHours + workHours;
if (workHours > 0) {
hoursElement.textContent = totalHours.toFixed(2) + ' h (' + halfHours.toFixed(2) + ' h Urlaub + ' + workHours.toFixed(2) + ' h)';
} else {
hoursElement.textContent = halfHours.toFixed(2) + ' h (Urlaub)';
}
currentEntries[date].total_hours = totalHours;
} else {
// Zurück zu normaler Anzeige basierend auf anderen Status
if (isSick) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)';
} else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)';
} else {
hoursElement.textContent = hours.toFixed(2) + ' h';
}
}
}
// 3. Tätigkeiten-Felder sofort deaktivieren/aktivieren (nur bei ganztägigem Urlaub)
const activityInputs = document.querySelectorAll(`input[data-date="${date}"][data-field^="activity"]`);
activityInputs.forEach(input => {
if (isFullDayVacation) {
input.disabled = true;
} else {
// Nur aktivieren wenn nicht krank
input.disabled = isSick;
}
});
// 4. Zeitfelder sofort deaktivieren/aktivieren (nur bei ganztägigem Urlaub)
const timeInputs = document.querySelectorAll(`input[data-date="${date}"][data-field="start_time"], input[data-date="${date}"][data-field="end_time"], input[data-date="${date}"][data-field="break_minutes"]`);
timeInputs.forEach(input => {
if (isFullDayVacation) {
input.disabled = true;
} else {
// Nur aktivieren wenn nicht krank
input.disabled = isSick;
}
});
// 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere
if (isFullDayVacation) {
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_desc"]`);
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours"]`);
if (descInput) {
descInput.value = 'Urlaub';
currentEntries[date].activity1_desc = 'Urlaub';
}
if (hoursInput) {
hoursInput.value = fullDayHours.toFixed(2);
currentEntries[date].activity1_hours = fullDayHours;
}
// Leere andere Tätigkeiten
for (let i = 2; i <= 5; i++) {
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
if (descInput) {
descInput.value = '';
currentEntries[date][`activity${i}_desc`] = null;
}
if (hoursInput) {
hoursInput.value = '';
currentEntries[date][`activity${i}_hours`] = 0;
}
if (projectInput) {
projectInput.value = '';
currentEntries[date][`activity${i}_project_number`] = null;
}
}
} else {
// Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren
for (let i = 1; i <= 5; i++) {
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 projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
if (descInput) {
descInput.value = '';
currentEntries[date][`activity${i}_desc`] = null;
}
if (hoursInput) {
hoursInput.value = '';
currentEntries[date][`activity${i}_hours`] = 0;
}
if (projectInput) {
projectInput.value = '';
currentEntries[date][`activity${i}_project_number`] = null;
}
}
}
}
const entry = currentEntries[date];
try {
const response = await fetch('/api/timesheet/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
start_time: start_time,
end_time: end_time,
break_minutes: break_minutes,
notes: notes,
activity1_desc: activities[0].desc,
activity1_hours: activities[0].hours,
activity1_project_number: activities[0].projectNumber,
activity2_desc: activities[1].desc,
activity2_hours: activities[1].hours,
activity2_project_number: activities[1].projectNumber,
activity3_desc: activities[2].desc,
activity3_hours: activities[2].hours,
activity3_project_number: activities[2].projectNumber,
activity4_desc: activities[3].desc,
activity4_hours: activities[3].hours,
activity4_project_number: activities[3].projectNumber,
activity5_desc: activities[4].desc,
activity5_hours: activities[4].hours,
activity5_project_number: activities[4].projectNumber,
overtime_taken_hours: overtime_taken_hours,
vacation_type: vacation_type,
sick_status: sick_status,
weekend_travel: weekend_travel
})
});
const result = await response.json();
if (result.success) {
// Aktualisiere Stunden-Anzeige
const hoursElement = document.getElementById(`hours_${date}`);
if (hoursElement && result.total_hours !== undefined) {
// Prüfe ob Urlaub oder Krank aktiv ist, um das richtige Label anzuzeigen
const entry = currentEntries[date] || {};
const vacationType = entry.vacation_type || '';
const isSick = entry.sick_status || false;
const isHoliday = currentHolidayDates.has(date);
const isFullDayVacation = vacationType === 'full';
const isHalfDayVacation = vacationType === 'half';
const fullDayHours = getFullDayHours();
let hoursText = result.total_hours.toFixed(2) + ' h';
if (isFullDayVacation) {
hoursText = fullDayHours.toFixed(2) + ' h (Urlaub)';
} else if (isHalfDayVacation) {
// Bei halbem Tag Urlaub: result.total_hours enthält nur die gearbeiteten Stunden
// Die Urlaubsstunden müssen addiert werden
const halfHours = fullDayHours / 2;
const workHours = result.total_hours || 0; // Das sind die gearbeiteten Stunden
const totalHours = halfHours + workHours; // Gesamt = Urlaub + gearbeitet
if (workHours > 0.01) {
hoursText = totalHours.toFixed(2) + ' h (' + halfHours.toFixed(2) + ' h Urlaub + ' + workHours.toFixed(2) + ' h)';
} else {
hoursText = halfHours.toFixed(2) + ' h (Urlaub)';
}
// Aktualisiere currentEntries mit den Gesamtstunden
currentEntries[date].total_hours = totalHours;
} else if (isSick) {
hoursText = fullDayHours.toFixed(2) + ' h (Krank)';
} else if (isHoliday && result.total_hours <= fullDayHours) {
hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && result.total_hours > fullDayHours) {
const overtime = result.total_hours - fullDayHours;
hoursText = fullDayHours.toFixed(2) + ' + ' + overtime.toFixed(2) + ' h (Überst.)';
}
hoursElement.textContent = hoursText;
// total_hours wurde bereits für halben Tag Urlaub gesetzt, sonst verwende result.total_hours
if (!isHalfDayVacation) {
currentEntries[date].total_hours = result.total_hours;
}
}
// Gesamtstunden neu berechnen
let totalHours = 0;
Object.values(currentEntries).forEach(e => {
totalHours += e.total_hours || 0;
});
document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h';
// Überstunden-Anzeige aktualisieren (bei jeder Änderung)
updateOvertimeDisplay();
// Wenn vacation_type geändert wurde, Statistiken aktualisieren (für verplante Tage)
if (input.dataset.field === 'vacation_type') {
loadUserStats();
}
// Submit-Button Status prüfen (nach jedem Speichern)
checkWeekComplete();
// Visuelles Feedback
input.style.backgroundColor = '#d4edda';
setTimeout(() => {
input.style.backgroundColor = '';
}, 500);
// Statistiken aktualisieren (nur wenn es sich um eingereichte Wochen handelt)
// Wir aktualisieren die Statistiken nicht bei jedem Speichern, da sie nur für eingereichte Wochen relevant sind
// Die Statistiken werden beim Laden der Seite und nach dem Abschicken aktualisiert
}
} catch (error) {
console.error('Fehler beim Speichern:', error);
alert('Fehler beim Speichern');
}
}
// Prüfen ob alle Werktage der Woche ausgefüllt sind
function checkWeekComplete() {
const startDate = new Date(currentWeekStart);
let allWeekdaysFilled = true;
const missingFields = [];
// Prüfe nur so viele Tage wie Arbeitstage pro Woche festgelegt sind
// Samstag und Sonntag sind optional
const requiredDays = userArbeitstage || 5; // Fallback auf 5 wenn nicht gesetzt
for (let i = 0; i < requiredDays; i++) {
const date = new Date(startDate);
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;
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
const vacationValue = vacationSelect ? vacationSelect.value : (vacationType || '');
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
// 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)
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0);
// Berechne fullDayHours (normalerweise 8 Stunden)
const fullDayHours = getFullDayHours();
// Wenn Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt
if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
continue; // Tag ist ausgefüllt (Überstunden = ganzer Tag)
}
// Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle)
// Auch bei manueller Eingabe werden die Werte hier erkannt
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
// Hole die Werte direkt aus den Input-Feldern
const startTime = startInput ? (startInput.value || '').trim() : '';
const endTime = endInput ? (endInput.value || '').trim() : '';
// Debug-Ausgabe - zeigt auch den tatsächlichen Wert im Input-Feld
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, {
startInputExists: !!startInput,
endInputExists: !!endInput,
startInputValue: startInput ? startInput.value : 'N/A',
endInputValue: endInput ? endInput.value : 'N/A',
vacationValue: vacationValue,
overtimeValue: overtimeValue,
fullDayHours: fullDayHours
});
// Wenn Überstunden > fullDayHours, dann müssen Start/Ende vorhanden sein
if (overtimeValue > fullDayHours) {
if (!startTime || !endTime || startTime === '' || endTime === '') {
allWeekdaysFilled = false;
missingFields.push(formatDateDE(dateStr) + ' (bei Überstunden > ' + fullDayHours.toFixed(2) + 'h müssen Start/Ende vorhanden sein)');
continue; // Weiter zum nächsten Tag
}
}
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
// (außer wenn Überstunden = fullDayHours eingetragen sind, dann sind Start/Ende nicht nötig)
if (!startTime || !endTime || startTime === '' || endTime === '') {
allWeekdaysFilled = false;
missingFields.push(formatDateDE(dateStr));
}
}
// Prüfe ob die Woche bereits eingereicht wurde (für Anzeige, aber Button bleibt aktiv für neue Versionen)
const weekIsSubmitted = currentEntries._weekSubmitted === true;
const submitButton = document.getElementById('submitWeek');
if (submitButton) {
// Button nur deaktivieren wenn nicht alle Felder ausgefüllt sind
// Resubmission ist erlaubt, da Versionierung unterstützt wird
submitButton.disabled = !allWeekdaysFilled;
if (!allWeekdaysFilled) {
const requiredDaysText = requiredDays === 1 ? '1 Tag' : `${requiredDays} Tage`;
submitButton.title = `Bitte füllen Sie alle ${requiredDaysText} (Start- und Endzeit) aus. Wochenende ist optional. Fehlend: ${missingFields.join(', ')}`;
} else if (weekIsSubmitted) {
submitButton.title = 'Diese Woche wurde bereits eingereicht. Beim erneuten Abschicken wird eine neue Version erstellt.';
} else {
submitButton.title = '';
}
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)
window.submitWeekHandler = function(e) {
e.preventDefault();
e.stopPropagation();
console.log('submitWeekHandler wurde aufgerufen');
const submitButton = document.getElementById('submitWeek');
// Auch wenn der Button disabled ist, versuchen wir zu prüfen was fehlt
if (submitButton) {
if (!submitButton.disabled) {
submitWeek();
} else {
// Button ist disabled - zeige was fehlt
console.warn('Button ist disabled - zeige fehlende Felder');
// Rufe submitWeek auf, um die Validierung durchzuführen (wird wegen fehlender Felder abgebrochen)
submitWeek();
}
} else {
console.error('Button nicht gefunden');
alert('Fehler: Button nicht gefunden');
}
return false;
};
// Woche abschicken
async function submitWeek() {
console.log('submitWeek() wurde aufgerufen');
const startDate = new Date(currentWeekStart);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
console.log('Prüfe Validierung für Woche:', currentWeekStart);
// Frontend-Validierung: Prüfen ob so viele Tage ausgefüllt sind wie Arbeitstage pro Woche festgelegt sind
let missingFields = [];
let firstMissingInput = null;
// Entferne vorherige Fehlermarkierungen
document.querySelectorAll('.missing-field').forEach(el => {
el.classList.remove('missing-field');
el.style.borderColor = '';
el.style.backgroundColor = '';
});
const requiredDays = userArbeitstage || 5; // Fallback auf 5 wenn nicht gesetzt
for (let i = 0; i < requiredDays; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
const dateStr = formatDate(date);
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"]`);
const vacationValue = vacationSelect ? vacationSelect.value : (entry.vacation_type || '');
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
// 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)
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0);
const fullDayHours = getFullDayHours();
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt
if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag)
}
// Prüfe IMMER direkt die Input-Felder im DOM - auch bei manueller Eingabe
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
// Hole die Werte direkt aus den Input-Feldern (nicht aus currentEntries!)
const startTime = startInput ? (startInput.value || '').trim() : '';
const endTime = endInput ? (endInput.value || '').trim() : '';
// Debug-Ausgabe mit detaillierten Informationen
console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, {
startInputExists: !!startInput,
endInputExists: !!endInput,
startInputValue: startInput ? `"${startInput.value}"` : 'N/A',
endInputValue: endInput ? `"${endInput.value}"` : 'N/A',
startInputType: startInput ? typeof startInput.value : 'N/A',
vacationValue: vacationValue,
overtimeValue: overtimeValue,
fullDayHours: fullDayHours
});
const missing = [];
if (!startTime || startTime === '') {
missing.push('Startzeit');
if (startInput) {
startInput.classList.add('missing-field');
startInput.style.borderColor = '#dc3545';
startInput.style.backgroundColor = '#fff5f5';
if (!firstMissingInput) firstMissingInput = startInput;
}
}
if (!endTime || endTime === '') {
missing.push('Endzeit');
if (endInput) {
endInput.classList.add('missing-field');
endInput.style.borderColor = '#dc3545';
endInput.style.backgroundColor = '#fff5f5';
if (!firstMissingInput) firstMissingInput = endInput;
}
}
if (missing.length > 0) {
missingFields.push(`${weekday} (${dateDisplay}): ${missing.join(' und ')}`);
}
}
if (missingFields.length > 0) {
console.warn('Fehlende Felder:', missingFields);
// Scroll zum ersten fehlenden Feld
if (firstMissingInput) {
firstMissingInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstMissingInput.focus(), 300);
}
// Detaillierte Fehlermeldung
const requiredDaysText = requiredDays === 1 ? '1 Tag' : `${requiredDays} Tage`;
const message = `❌ Bitte füllen Sie alle ${requiredDaysText} vollständig aus!\n\n` +
`Fehlende Eingaben:\n${missingFields.map((field, index) => `\n${index + 1}. ${field}`).join('')}\n\n` +
`Die fehlenden Felder wurden rot markiert.\n` +
`Hinweis: Samstag und Sonntag sind optional.`;
alert(message);
return;
}
console.log('Alle Werktage sind ausgefüllt');
// Prüfe ob bereits eine Version existiert
const hasExistingVersion = currentEntries._hasExistingVersion || false;
if (hasExistingVersion) {
// Zeige Modal für Grund-Eingabe
showVersionReasonModal((reason) => {
if (reason) {
submitWeekWithReason(reason);
}
});
} else {
// Erste Version - normale Bestätigung
const confirmed = confirm(
`Möchten Sie die Stundenerfassung für die Woche vom ${formatDateDE(currentWeekStart)} bis ${formatDateDE(formatDate(endDate))} wirklich abschicken?`
);
if (!confirmed) {
console.log('Benutzer hat abgebrochen');
return;
}
submitWeekWithReason(null);
}
}
// Modal für Grund-Eingabe anzeigen
function showVersionReasonModal(callback) {
// Erstelle Modal
const modal = document.createElement('div');
modal.id = 'versionReasonModal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: white;
padding: 30px;
border-radius: 8px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;
modalContent.innerHTML = `
Neue Version einreichen
Es existiert bereits eine Version für diese Woche. Bitte geben Sie einen Grund an, warum Sie eine neue Version einreichen:
`;
modal.appendChild(modalContent);
document.body.appendChild(modal);
const reasonInput = document.getElementById('versionReasonInput');
reasonInput.focus();
// Event-Handler
document.getElementById('cancelReasonBtn').addEventListener('click', () => {
document.body.removeChild(modal);
callback(null);
});
document.getElementById('submitReasonBtn').addEventListener('click', () => {
const reason = reasonInput.value.trim();
if (!reason) {
alert('Bitte geben Sie einen Grund für die neue Version an.');
reasonInput.focus();
return;
}
document.body.removeChild(modal);
callback(reason);
});
// Enter-Taste im Textarea
reasonInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
document.getElementById('submitReasonBtn').click();
}
});
// ESC-Taste zum Schließen
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
callback(null);
}
});
}
// Woche mit Grund abschicken
async function submitWeekWithReason(versionReason) {
const startDate = new Date(currentWeekStart);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
console.log('Sende Anfrage an Server...');
try {
const response = await fetch('/api/timesheet/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
week_start: currentWeekStart,
week_end: formatDate(endDate),
version_reason: versionReason || null
})
});
console.log('Server-Antwort erhalten, Status:', response.status);
const result = await response.json();
console.log('Server-Antwort:', result);
if (result.success) {
const versionText = result.version ? ` (Version ${result.version})` : '';
alert(`Stundenzettel wurde erfolgreich eingereicht${versionText}!`);
loadWeek(); // Neu laden um Status zu aktualisieren
// Statistiken aktualisieren
loadUserStats();
} else {
console.error('Fehler-Details:', result);
alert(result.error || 'Fehler beim Einreichen des Stundenzettels');
}
} catch (error) {
console.error('Fehler beim Abschicken:', error);
alert('Fehler beim Abschicken: ' + error.message);
}
}
// Überstunden-Eingabefeld ein-/ausblenden
function toggleOvertimeInput(dateStr) {
const inputDiv = document.getElementById(`overtime-input-${dateStr}`);
if (inputDiv) {
if (inputDiv.style.display === 'none' || !inputDiv.style.display) {
inputDiv.style.display = 'inline-block';
const input = inputDiv.querySelector('input');
if (input) {
input.focus();
}
} else {
inputDiv.style.display = 'none';
// Wert löschen wenn ausgeblendet
const input = inputDiv.querySelector('input');
if (input) {
input.value = '';
// Speichern
if (currentEntries[dateStr]) {
currentEntries[dateStr].overtime_taken_hours = null;
saveEntry(input);
}
}
}
}
}
// Urlaub-Auswahl ein-/ausblenden
function toggleVacationSelect(dateStr) {
const selectDiv = document.getElementById(`vacation-select-${dateStr}`);
if (selectDiv) {
if (selectDiv.style.display === 'none' || !selectDiv.style.display) {
selectDiv.style.display = 'inline-block';
const select = selectDiv.querySelector('select');
if (select) {
select.focus();
}
} else {
selectDiv.style.display = 'none';
// Wert löschen wenn ausgeblendet
const select = selectDiv.querySelector('select');
if (select) {
select.value = '';
// Speichern
if (currentEntries[dateStr]) {
currentEntries[dateStr].vacation_type = null;
saveEntry(select);
}
}
}
}
}
// 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 = '';
}
// SOFORTIGE DOM-UPDATES
// 1. Datum-Zelle: "(Krank)" in rot hinzufügen/entfernen
const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
if (startInput) {
const row = startInput.closest('tr');
if (row) {
const dateCell = row.querySelector('td:nth-child(2)'); // Zweite Spalte ist das Datum
if (dateCell) {
let dateText = dateCell.innerHTML;
const sickSpan = '(Krank)';
if (newStatus) {
// Prüfe ob bereits vorhanden
if (!dateText.includes('(Krank)')) {
dateText += ' ' + sickSpan;
dateCell.innerHTML = dateText;
}
} else {
// Entferne "(Krank)" Span
dateText = dateText.replace(/ \(Krank\)<\/span>/g, '');
dateCell.innerHTML = dateText;
}
}
}
}
// 2. Stunden-Anzeige sofort aktualisieren
const hoursElement = document.getElementById(`hours_${dateStr}`);
if (hoursElement) {
const fullDayHours = getFullDayHours();
const entry = currentEntries[dateStr] || {};
const vacationType = entry.vacation_type || '';
const isHoliday = currentHolidayDates.has(dateStr);
const hours = entry.total_hours || 0;
const isFullDayVacation = vacationType === 'full';
if (newStatus) {
// Krank: Zeige fullDayHours mit "(Krank)" Label
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)';
currentEntries[dateStr].total_hours = fullDayHours;
} else {
// Zurück zu normaler Anzeige basierend auf anderen Status
if (isFullDayVacation) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)';
} else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)';
} else {
hoursElement.textContent = hours.toFixed(2) + ' h';
}
}
}
// 3. Tätigkeiten-Felder sofort deaktivieren/aktivieren
const activityInputs = document.querySelectorAll(`input[data-date="${dateStr}"][data-field^="activity"]`);
activityInputs.forEach(input => {
if (newStatus) {
input.disabled = true;
} else {
input.disabled = false;
}
});
// 4. Zeitfelder sofort deaktivieren/aktivieren
const timeInputs = document.querySelectorAll(`input[data-date="${dateStr}"][data-field="start_time"], input[data-date="${dateStr}"][data-field="end_time"], input[data-date="${dateStr}"][data-field="break_minutes"]`);
timeInputs.forEach(input => {
if (newStatus) {
input.disabled = true;
} else {
input.disabled = false;
}
});
// 5. Bei Abwahl: Alle Tätigkeitsfelder leeren
if (!newStatus) {
// Leere alle Tätigkeitsfelder
for (let i = 1; i <= 5; i++) {
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 projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
if (descInput) {
descInput.value = '';
currentEntries[dateStr][`activity${i}_desc`] = null;
}
if (hoursInput) {
hoursInput.value = '';
currentEntries[dateStr][`activity${i}_hours`] = 0;
}
if (projectInput) {
projectInput.value = '';
currentEntries[dateStr][`activity${i}_project_number`] = null;
}
}
} else {
// Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere
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 fullDayHours = getFullDayHours();
if (descInput) {
descInput.value = 'Krank';
currentEntries[dateStr].activity1_desc = 'Krank';
}
if (hoursInput) {
hoursInput.value = fullDayHours.toFixed(2);
currentEntries[dateStr].activity1_hours = fullDayHours;
}
// Leere andere Tätigkeiten
for (let i = 2; i <= 5; i++) {
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 projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
if (descInput) {
descInput.value = '';
currentEntries[dateStr][`activity${i}_desc`] = null;
}
if (hoursInput) {
hoursInput.value = '';
currentEntries[dateStr][`activity${i}_hours`] = 0;
}
if (projectInput) {
projectInput.value = '';
currentEntries[dateStr][`activity${i}_project_number`] = null;
}
}
}
// 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
async function loadPingIP() {
try {
const response = await fetch('/api/user/ping-ip');
if (!response.ok) {
throw new Error('Fehler beim Laden der IP-Adresse');
}
const data = await response.json();
const pingIpInput = document.getElementById('pingIpInput');
if (pingIpInput) {
pingIpInput.value = data.ping_ip || '';
}
} catch (error) {
console.error('Fehler beim Laden der Ping-IP:', error);
}
}
// Client-IP ermitteln und eintragen (global für onclick)
window.detectClientIP = async function() {
const pingIpInput = document.getElementById('pingIpInput');
const detectButton = document.querySelector('button[onclick*="detectClientIP"]');
if (!pingIpInput) {
return;
}
// Button-Status während des Ladens
if (detectButton) {
const originalText = detectButton.textContent;
detectButton.textContent = 'Ermittle...';
detectButton.disabled = true;
try {
const response = await fetch('/api/user/client-ip');
if (!response.ok) {
throw new Error('Fehler beim Abrufen der IP-Adresse');
}
const data = await response.json();
if (data.client_ip && data.client_ip !== 'unknown') {
// IP in das Eingabefeld eintragen
pingIpInput.value = data.client_ip;
// Erfolgs-Feedback
detectButton.textContent = 'IP ermittelt!';
detectButton.style.backgroundColor = '#27ae60';
setTimeout(() => {
detectButton.textContent = originalText;
detectButton.style.backgroundColor = '#3498db';
detectButton.disabled = false;
}, 2000);
} else {
alert('IP-Adresse konnte nicht ermittelt werden.');
detectButton.textContent = originalText;
detectButton.disabled = false;
}
} catch (error) {
console.error('Fehler beim Ermitteln der Client-IP:', error);
alert('Fehler beim Ermitteln der IP-Adresse');
if (detectButton) {
detectButton.textContent = originalText;
detectButton.disabled = false;
}
}
}
};
// Ping-IP speichern (global für onclick)
window.savePingIP = async function() {
const pingIpInput = document.getElementById('pingIpInput');
if (!pingIpInput) {
return;
}
const pingIp = pingIpInput.value.trim();
// Finde den Button (nächstes Geschwisterelement oder über Parent)
const button = pingIpInput.parentElement?.querySelector('button') ||
document.querySelector('button[onclick*="savePingIP"]');
try {
const response = await fetch('/api/user/ping-ip', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ping_ip: pingIp })
});
const result = await response.json();
if (!response.ok) {
alert(result.error || 'Fehler beim Speichern der IP-Adresse');
return;
}
// Erfolgs-Feedback
if (button) {
const originalText = button.textContent;
button.textContent = 'Gespeichert!';
button.style.backgroundColor = '#27ae60';
setTimeout(() => {
button.textContent = originalText;
button.style.backgroundColor = '';
}, 2000);
}
console.log('Ping-IP gespeichert:', result.ping_ip);
} catch (error) {
console.error('Fehler beim Speichern der Ping-IP:', error);
alert('Fehler beim Speichern der IP-Adresse');
}
};