// Dashboard JavaScript let currentWeekStart = getMonday(new Date()); let currentEntries = {}; let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD) let userWochenstunden = 0; // Wochenstunden des Users let userArbeitstage = 5; // Arbeitstage pro Woche des Users (Standard: 5) let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal) let defaultBreakMinutes = 30; // Standard-Pausenzeit des Mitarbeiters (Vorbelegung) let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version // Wochenend-Prozentsätze laden async function loadWeekendPercentages() { try { const response = await fetch('/api/user/weekend-percentages'); if (!response.ok) { throw new Error('Fehler beim Laden der Wochenend-Prozentsätze'); } const data = await response.json(); weekendPercentages.saturday = data.saturday_percentage || 100; weekendPercentages.sunday = data.sunday_percentage || 100; } catch (error) { console.error('Fehler beim Laden der Wochenend-Prozentsätze:', error); // Standardwerte verwenden weekendPercentages.saturday = 100; weekendPercentages.sunday = 100; } } // Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück function getWeekendPercentage(date) { const day = date.getDay(); if (day === 6) { // Samstag return weekendPercentages.saturday; } else if (day === 0) { // Sonntag return weekendPercentages.sunday; } return 100; // Kein Wochenende = 100% (normal) } // Statistiken laden async function loadUserStats() { try { const response = await fetch('/api/user/stats'); if (!response.ok) { throw new Error('Fehler beim Laden der Statistiken'); } const stats = await response.json(); // Überstunden anzeigen const currentOvertimeEl = document.getElementById('currentOvertime'); if (currentOvertimeEl) { const overtime = stats.currentOvertime || 0; currentOvertimeEl.textContent = (overtime >= 0 ? '+' : '') + formatHoursMin(overtime); currentOvertimeEl.style.color = overtime >= 0 ? '#27ae60' : '#e74c3c'; // Auch die Border-Farbe des Cards anpassen const overtimeCard = currentOvertimeEl.closest('.stat-card'); if (overtimeCard) { overtimeCard.style.borderLeftColor = overtime >= 0 ? '#27ae60' : '#e74c3c'; } } // Urlaubstage anzeigen const remainingVacationEl = document.getElementById('remainingVacation'); if (remainingVacationEl) { remainingVacationEl.textContent = (stats.remainingVacation || 0).toFixed(1); } const totalVacationEl = document.getElementById('totalVacation'); if (totalVacationEl) { totalVacationEl.textContent = (stats.urlaubstage || 0).toFixed(1); } // Verplante Urlaubstage anzeigen const plannedVacationEl = document.getElementById('plannedVacation'); if (plannedVacationEl) { plannedVacationEl.textContent = (stats.plannedVacationDays || 0).toFixed(1); } // Kalenderwochen anzeigen const plannedWeeksEl = document.getElementById('plannedWeeks'); if (plannedWeeksEl) { if (stats.plannedWeeks && stats.plannedWeeks.length > 0) { const weeksHTML = stats.plannedWeeks.map(w => { const daysText = w.days === 1 ? '1 Tag' : `${w.days.toFixed(1)} Tage`; return `${w.year} KW${String(w.week).padStart(2, '0')} (${daysText})`; }).join('
'); plannedWeeksEl.innerHTML = weeksHTML; plannedWeeksEl.style.display = 'block'; } else { plannedWeeksEl.textContent = ''; plannedWeeksEl.style.display = 'none'; } } } catch (error) { console.error('Fehler beim Laden der Statistiken:', error); // Fehlerbehandlung: Zeige "-" oder "Fehler" const currentOvertimeEl = document.getElementById('currentOvertime'); if (currentOvertimeEl) currentOvertimeEl.textContent = '-'; const remainingVacationEl = document.getElementById('remainingVacation'); if (remainingVacationEl) remainingVacationEl.textContent = '-'; const totalVacationEl = document.getElementById('totalVacation'); if (totalVacationEl) totalVacationEl.textContent = '-'; const plannedVacationEl = document.getElementById('plannedVacation'); if (plannedVacationEl) plannedVacationEl.textContent = '-'; const plannedWeeksEl = document.getElementById('plannedWeeks'); if (plannedWeeksEl) { plannedWeeksEl.textContent = ''; plannedWeeksEl.style.display = 'none'; } } } // Beim Laden der Seite document.addEventListener('DOMContentLoaded', async function() { // Letzte Woche vom Server laden try { const response = await fetch('/api/user/last-week'); const data = await response.json(); if (data.last_week_start) { // Prüfe ob last_week_start wirklich ein Montag ist if (isMonday(data.last_week_start)) { currentWeekStart = data.last_week_start; } else { // Korrigiere zu Montag falls nicht console.warn('last_week_start war kein Montag, korrigiere:', data.last_week_start); currentWeekStart = getMonday(data.last_week_start); // Speichere die korrigierte Woche saveLastWeek(); } } } catch (error) { console.warn('Konnte letzte Woche nicht vom Server laden:', error); } // Stelle sicher, dass currentWeekStart immer ein Montag ist if (currentWeekStart && !isMonday(currentWeekStart)) { console.warn('currentWeekStart war kein Montag, korrigiere:', currentWeekStart); currentWeekStart = getMonday(currentWeekStart); saveLastWeek(); } // Ping-IP laden loadPingIP(); // Wochenend-Prozentsätze laden loadWeekendPercentages(); // Statistiken laden loadUserStats(); loadWeek(); document.getElementById('prevWeek').addEventListener('click', function() { // Parse als lokales Datum const parts = currentWeekStart.split('-'); const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); date.setDate(date.getDate() - 7); // Stelle sicher, dass es ein Montag ist currentWeekStart = getMonday(date); saveLastWeek(); loadWeek(); }); document.getElementById('nextWeek').addEventListener('click', function() { // Parse als lokales Datum const parts = currentWeekStart.split('-'); const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); date.setDate(date.getDate() + 7); // Stelle sicher, dass es ein Montag ist currentWeekStart = getMonday(date); saveLastWeek(); loadWeek(); }); function showGoToWeekError(msg) { const el = document.getElementById('goToWeekError'); if (el) { el.textContent = msg; el.style.display = 'inline'; } } function clearGoToWeekError() { const el = document.getElementById('goToWeekError'); if (el) { el.textContent = ''; el.style.display = 'none'; } } function goToWeek() { clearGoToWeekError(); const input = document.getElementById('goToWeekInput'); if (!input) return; const raw = (input.value || '').trim(); if (!raw) { showGoToWeekError('Bitte KW eingeben (z. B. 12 oder 2025 12).'); return; } let year = null; let week = null; const parts = raw.split(/[\s\/]+/).filter(Boolean).map(p => parseInt(p, 10)); if (parts.length === 1 && !isNaN(parts[0])) { week = parts[0]; year = currentWeekStart ? parseInt(currentWeekStart.split('-')[0], 10) : new Date().getFullYear(); } else if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { if (parts[0] >= 1000) { year = parts[0]; week = parts[1]; } else { week = parts[0]; year = parts[1]; } } if (year == null || week == null || week < 1 || week > 53 || year < 2000 || year > 2100) { showGoToWeekError('Ungültige Eingabe. KW 1–53, Jahr 2000–2100 (z. B. 12 oder 2025 12).'); return; } currentWeekStart = getWeekStartFromYearAndWeek(year, week); saveLastWeek(); loadWeek(); } const goToWeekBtn = document.getElementById('goToWeekBtn'); if (goToWeekBtn) goToWeekBtn.addEventListener('click', goToWeek); const goToWeekInput = document.getElementById('goToWeekInput'); if (goToWeekInput) { goToWeekInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); goToWeek(); } }); } // Event-Listener für Submit-Button - mit Event-Delegation für bessere Zuverlässigkeit document.addEventListener('click', function(e) { if (e.target && e.target.id === 'submitWeek') { e.preventDefault(); e.stopPropagation(); const submitButton = e.target; console.log('Submit-Button wurde geklickt!'); console.log('Button disabled?', submitButton.disabled); console.log('Button element:', submitButton); if (!submitButton.disabled) { submitWeek(); } else { console.warn('Submit-Button ist deaktiviert!'); const tooltip = submitButton.title || 'Bitte füllen Sie alle Werktage aus.'; alert(tooltip); } } }); // Zusätzlicher direkter Event-Listener als Fallback const submitButton = document.getElementById('submitWeek'); if (submitButton) { submitButton.onclick = function(e) { e.preventDefault(); e.stopPropagation(); console.log('Submit-Button onclick wurde ausgelöst!'); if (!this.disabled) { submitWeek(); } return false; }; console.log('Submit-Button Event-Listener gesetzt'); } 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 async function saveLastWeek() { try { await fetch('/api/user/last-week', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ week_start: currentWeekStart }) }); } catch (error) { console.warn('Konnte letzte Woche nicht auf dem Server speichern:', error); } } // Prüft ob ein Datum ein Montag ist (1 = Montag) function isMonday(dateStr) { const d = new Date(dateStr + 'T00:00:00'); // Lokale Zeit verwenden, nicht UTC return d.getDay() === 1; } // Montag der aktuellen Woche ermitteln (robust gegen Zeitzonenprobleme) function getMonday(date) { // Wenn date bereits ein String ist (YYYY-MM-DD), parsen wir es als lokales Datum let d; if (typeof date === 'string') { // Parse als lokales Datum, nicht UTC const parts = date.split('-'); d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); } else { d = new Date(date); } // Stelle sicher, dass wir mit lokaler Zeit arbeiten const day = d.getDay(); // 0 = Sonntag, 1 = Montag, ..., 6 = Samstag // Berechne Differenz zum Montag: Montag = 1, also diff = 1 - day // Aber: Sonntag = 0, also für Sonntag: diff = 1 - 0 = 1, aber wir wollen -6 Tage zurück const diff = day === 0 ? -6 : 1 - day; d.setDate(d.getDate() + diff); // Format als YYYY-MM-DD in lokaler Zeit const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const dayOfMonth = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${dayOfMonth}`; } // Kalenderwoche berechnen (ISO 8601 - Woche beginnt Montag) function getCalendarWeek(dateStr) { const date = new Date(dateStr); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); return weekNo; } // Montag (week_start) aus Jahr und Kalenderwoche (ISO 8601) function getWeekStartFromYearAndWeek(year, weekNumber) { const jan4 = new Date(Date.UTC(year, 0, 4)); const jan4Day = jan4.getUTCDay() || 7; const daysToMonday = jan4Day === 1 ? 0 : 1 - jan4Day; const firstMonday = new Date(Date.UTC(year, 0, 4 + daysToMonday)); const weekMonday = new Date(firstMonday); weekMonday.setUTCDate(firstMonday.getUTCDate() + (weekNumber - 1) * 7); const y = weekMonday.getUTCFullYear(); const m = String(weekMonday.getUTCMonth() + 1).padStart(2, '0'); const d = String(weekMonday.getUTCDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } // Datum formatieren (YYYY-MM-DD) function formatDate(date) { const d = new Date(date); return d.toISOString().split('T')[0]; } // Datum formatieren (DD.MM.YYYY) function formatDateDE(dateStr) { const d = new Date(dateStr); return d.toLocaleDateString('de-DE'); } // Wochentag ermitteln function getWeekday(dateStr) { const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; const d = new Date(dateStr); return days[d.getDay()]; } // Hilfsfunktion: Berechnet die Stunden pro Tag (Wochenarbeitszeit / Arbeitstage) function getFullDayHours() { return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8; } // Woche laden async function loadWeek() { try { // User-Daten laden (Wochenstunden, Arbeitstage, Standard-Pausenzeit) try { const userResponse = await fetch('/api/user/data'); const userData = await userResponse.json(); userWochenstunden = userData.wochenstunden || 0; userArbeitstage = userData.arbeitstage || 5; defaultBreakMinutes = userData.default_break_minutes ?? 30; } catch (error) { console.warn('Konnte User-Daten nicht laden:', error); userWochenstunden = 0; userArbeitstage = 5; defaultBreakMinutes = 30; } const parts = currentWeekStart.split('-'); 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, latestSubmittedResponse] = await Promise.all([ fetch(`/api/timesheet/week/${currentWeekStart}`), 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 = {}; let weekIsSubmitted = false; entries.forEach(entry => { currentEntries[entry.date] = entry; // Prüfe ob die Woche bereits eingereicht wurde if (entry.week_submitted) { weekIsSubmitted = true; } }); // Speichere den Status ob die Woche bereits eingereicht wurde currentEntries._weekSubmitted = weekIsSubmitted; // Speichere ob bereits eine Version existiert (für Grund-Validierung) if (entries.length > 0) { currentEntries._hasExistingVersion = entries[0].has_existing_version || false; currentEntries._latestVersion = entries[0].latest_version || 0; } renderWeek(); } catch (error) { console.error('Fehler beim Laden der Woche:', error); alert('Fehler beim Laden der Daten'); } } // Woche rendern function renderWeek() { const startDate = new Date(currentWeekStart); const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 6); const calendarWeek = getCalendarWeek(currentWeekStart); const submittedText = currentEntries._weekSubmitted === true ? `
(bereits Abgegeben)` : ''; document.getElementById('weekTitle').innerHTML = `Kalenderwoche ${calendarWeek}
${formatDateDE(currentWeekStart)} - ${formatDateDE(formatDate(endDate))}${submittedText}`; let html = `
`; let totalHours = 0; let allWeekdaysFilled = true; // Hilfsfunktion zum Rendern eines einzelnen Tages function renderDay(i) { const date = new Date(startDate); date.setDate(date.getDate() + i); const dateStr = formatDate(date); const entry = currentEntries[dateStr] || {}; const startTime = entry.start_time || ''; const endTime = entry.end_time || ''; const breakMinutes = (entry.break_minutes != null && entry.break_minutes !== '') ? entry.break_minutes : defaultBreakMinutes; const hours = entry.total_hours || 0; const overtimeTaken = entry.overtime_taken_hours || ''; const vacationType = entry.vacation_type || ''; 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 = [ { desc: entry.activity1_desc || '', hours: entry.activity1_hours || 0, projectNumber: entry.activity1_project_number || '' }, { desc: entry.activity2_desc || '', hours: entry.activity2_hours || 0, projectNumber: entry.activity2_project_number || '' }, { desc: entry.activity3_desc || '', hours: entry.activity3_hours || 0, projectNumber: entry.activity3_project_number || '' }, { desc: entry.activity4_desc || '', hours: entry.activity4_hours || 0, projectNumber: entry.activity4_project_number || '' }, { desc: entry.activity5_desc || '', hours: entry.activity5_hours || 0, projectNumber: entry.activity5_project_number || '' } ]; // Prüfen ob Werktag (Montag-Freitag, i < 5) ausgefüllt ist // Bei Feiertag, ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt // Bei 8 Überstunden (ganzer Tag) gilt der Tag auch als ausgefüllt const overtimeValue = overtimeTaken ? parseFloat(overtimeTaken) : 0; const fullDayHours = getFullDayHours(); const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; if (i < 5 && !isHoliday && vacationType !== 'full' && !sickStatus && !isFullDayOvertime && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) { allWeekdaysFilled = false; } // Stunden zur Summe hinzufügen // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) // Feiertag Werktag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde) // Feiertag am Wochenende: keine Tagesarbeitsstunden, nur gearbeitete Stunden (z. B. Reise) // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt // Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage) let hoursToAdd = 0; if (isHoliday) { if (isWeekend) { hoursToAdd = hours || 0; // Feiertag am Wochenende: keine Tagesarbeitsstunden } else { hoursToAdd = fullDayHours + (hours || 0); // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) } } else { hoursToAdd = hours || 0; // 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; // Bearbeitung ist immer möglich, auch nach Abschicken // Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden) const isFullDayVacation = vacationType === 'full'; const isHalfDayVacation = vacationType === 'half'; const isSick = sickStatus === true || sickStatus === 1; const timeFieldsDisabled = (isFullDayVacation || isSick) ? 'disabled' : ''; const disabled = ''; const holidayLabel = isHoliday ? ' (Feiertag)' : ''; // Stunden-Anzeige für halben Tag Urlaub berechnen let hoursDisplay = ''; if (isFullDayVacation) { hoursDisplay = formatHoursMin(fullDayHours) + ' (Urlaub)'; } else if (isHalfDayVacation) { const halfHours = fullDayHours / 2; const workHours = hours || 0; // Das sind die gearbeiteten Stunden (ohne Urlaub) const totalHours = halfHours + workHours; if (workHours > 0.01) { hoursDisplay = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')'; } else { hoursDisplay = formatHoursMin(halfHours) + ' (Urlaub)'; } } else if (isSick) { hoursDisplay = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isHoliday && isWeekend) { // Feiertag am Wochenende: keine Tagesarbeitsstunden hoursDisplay = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { hoursDisplay = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { hoursDisplay = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { hoursDisplay = formatHoursMin(hours); } const requiredBreak = (startTime && endTime) ? calculateRequiredBreakMinutes(startTime, endTime) : null; const isBreakBelowLegal = requiredBreak !== null && breakMinutes < requiredBreak; const breakClass = isBreakBelowLegal ? 'break-below-legal' : ''; const breakTitle = isBreakBelowLegal ? ' title="Die Pausenzeit liegt unterhalb der gesetzlichen Vorgabe."' : ''; return ` `; } // Werktage (Montag bis Freitag) rendern for (let i = 0; i < 5; i++) { html += renderDay(i); } // Wochenende (Samstag und Sonntag) in zusammenklappbarer Sektion html += ` `; html += `
Tag Datum Start Ende Pause (Min) Stunden
${getWeekday(dateStr)} ${formatDateDE(dateStr)}${isFullDayVacation ? ' (Urlaub - ganzer Tag)' : ''}${isSick ? ' (Krank)' : ''}${holidayLabel} ${hoursDisplay}
Tätigkeiten:
${activities.map((activity, idx) => `
h
`).join('')}
h
${isWeekend ? `
` : ''}

Wochenende

`; document.getElementById('timesheetTable').innerHTML = html; document.getElementById('totalHours').textContent = formatHoursMin(totalHours); // Ü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: Werktag = Basis + gearbeitete Stunden; Wochenende = nur gearbeitete Stunden (keine Tagesarbeitsstunden) const dayOfWeek = date.getDay(); const isWeekendHoliday = (dayOfWeek === 6 || dayOfWeek === 0); 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; } if (isWeekendHoliday) { totalHours += worked; // Feiertag am Wochenende: keine Tagesarbeitsstunden } else { 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) // Genommene Überstunden werden abgezogen, um die Netto-Überstunden zu erhalten const overtimeHours = totalHoursWithVacation - adjustedSollStunden - overtimeTaken; // Ü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 === '+' ? '+' : '') + formatHoursMin(overtimeHours); overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c'; } // Gesamtstunden-Anzeige aktualisieren const totalHoursElement = document.getElementById('totalHours'); if (totalHoursElement) { totalHoursElement.textContent = formatHoursMin(totalHoursWithVacation); } } // Ü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 } // Aktualisiert die visuelle Kennzeichnung (nur rot + Tooltip wenn unter gesetzlicher Mindestpause) function updateBreakCompliance(dateStr) { const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`); if (!breakInput) return; breakInput.classList.remove('break-below-legal'); breakInput.removeAttribute('title'); const startTime = startInput && startInput.value ? startInput.value.trim() : ''; const endTime = endInput && endInput.value ? endInput.value.trim() : ''; if (!startTime || !endTime) return; const required = calculateRequiredBreakMinutes(startTime, endTime); if (required === null) return; const breakVal = breakInput.value ? (parseInt(breakInput.value, 10) || 0) : 0; if (breakVal < required) { breakInput.classList.add('break-below-legal'); breakInput.setAttribute('title', 'Die Pausenzeit liegt unterhalb der gesetzlichen Vorgabe.'); } } // Eintrag speichern async function saveEntry(input) { const date = input.dataset.date; 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); 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 = formatHoursMin(fullDayHours) + ' (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 = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')'; } else { hoursElement.textContent = formatHoursMin(halfHours) + ' (Urlaub)'; } currentEntries[date].total_hours = totalHours; } else { // Zurück zu normaler Anzeige basierend auf anderen Status const d = new Date(date); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isSick) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isWeekendHoliday) { hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { hoursElement.textContent = formatHoursMin(hours); } } } // 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 = formatHoursMin(result.total_hours); if (isFullDayVacation) { hoursText = formatHoursMin(fullDayHours) + ' (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 = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')'; } else { hoursText = formatHoursMin(halfHours) + ' (Urlaub)'; } // Aktualisiere currentEntries mit den Gesamtstunden currentEntries[date].total_hours = totalHours; } else if (isSick) { hoursText = formatHoursMin(fullDayHours) + ' (Krank)'; } else if (isHoliday) { const d = new Date(date); const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0); if (isWeekendHoliday) { hoursText = formatHoursMin(result.total_hours || 0) + ' (Feiertag)'; } else if (result.total_hours <= fullDayHours) { hoursText = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else { const overtime = result.total_hours - fullDayHours; hoursText = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(overtime) + ' (Ü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 = formatHoursMin(totalHours); // Ü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(); if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') { updateBreakCompliance(date); } // 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 > ' + formatHoursMin(fullDayHours) + ' 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 = formatHoursMin(fullDayHours) + ' (Krank)'; currentEntries[dateStr].total_hours = fullDayHours; } else { // Zurück zu normaler Anzeige basierend auf anderen Status const d = new Date(dateStr); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isFullDayVacation) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Urlaub)'; } else if (isWeekendHoliday) { hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)'; } else if (isHoliday && !hours) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)'; } else if (isHoliday && hours) { hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)'; } else { hoursElement.textContent = formatHoursMin(hours); } } } // 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'); } };