Files
SDSStundenerfassung/public/js/dashboard.js

2300 lines
96 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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('<br>');
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 153, Jahr 20002100 (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
? `<br><span style="color: #28a745;">(bereits Abgegeben)</span>`
: '';
document.getElementById('weekTitle').innerHTML =
`Kalenderwoche ${calendarWeek}<br>${formatDateDE(currentWeekStart)} - ${formatDateDE(formatDate(endDate))}${submittedText}`;
let html = `
<div class="timesheet-grid">
<table>
<thead>
<tr>
<th>Tag</th>
<th>Datum</th>
<th>Start</th>
<th>Ende</th>
<th>Pause (Min)</th>
<th>Stunden</th>
</tr>
</thead>
<tbody>
`;
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 ? ' <span style="color: #6f42c1;">(Feiertag)</span>' : '';
// 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 `
<tr>
<td><strong>${getWeekday(dateStr)}</strong></td>
<td>${formatDateDE(dateStr)}${isFullDayVacation ? ' <span style="color: #28a745;">(Urlaub - ganzer Tag)</span>' : ''}${isSick ? ' <span style="color: #e74c3c;">(Krank)</span>' : ''}${holidayLabel}</td>
<td>
<input type="time" id="start_time_${dateStr}" name="start_time_${dateStr}" value="${startTime}"
data-date="${dateStr}" data-field="start_time"
step="60"
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
</td>
<td>
<input type="time" id="end_time_${dateStr}" name="end_time_${dateStr}" value="${endTime}"
data-date="${dateStr}" data-field="end_time"
step="60"
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)" onblur="saveEntry(this); checkWeekComplete();">
</td>
<td>
<input type="number" id="break_minutes_${dateStr}" name="break_minutes_${dateStr}" value="${breakMinutes}" min="0" step="15" class="${breakClass}"${breakTitle}
data-date="${dateStr}" data-field="break_minutes"
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)">
</td>
<td><strong id="hours_${dateStr}">${hoursDisplay}</strong></td>
</tr>
<tr class="activities-row">
<td colspan="6" class="activities-cell">
<div class="activities-form">
<div class="activities-header"><strong>Tätigkeiten:</strong></div>
${activities.map((activity, idx) => `
<div class="activity-row">
<div class="activity-desc">
<input type="text"
data-date="${dateStr}"
data-field="activity${idx + 1}_desc"
value="${activity.desc || ''}"
placeholder="Tätigkeit ${idx + 1}"
${timeFieldsDisabled} ${disabled}
onblur="saveEntry(this)"
class="activity-input">
</div>
<div class="activity-project">
<input type="text"
data-date="${dateStr}"
data-field="activity${idx + 1}_project_number"
value="${activity.projectNumber || ''}"
placeholder="Projektnummer"
${timeFieldsDisabled} ${disabled}
onblur="saveEntry(this)"
class="activity-project-input">
</div>
<div class="activity-hours">
<input type="number"
data-date="${dateStr}"
data-field="activity${idx + 1}_hours"
value="${activity.hours > 0 ? activity.hours.toFixed(2) : ''}"
min="0"
step="0.25"
placeholder="0.00"
${timeFieldsDisabled} ${disabled}
onblur="saveEntry(this)"
oninput="updateOvertimeDisplay();"
onchange="updateOvertimeDisplay();"
class="activity-hours-input">
<span class="activity-hours-label">h</span>
</div>
</div>
`).join('')}
</div>
<div class="overtime-vacation-controls" style="margin-top: 15px; display: flex; gap: 15px; align-items: center;">
<div class="overtime-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleOvertimeInput('${dateStr}')" style="margin-right: 5px;">
Überstunden
</button>
<div id="overtime-input-${dateStr}" style="display: ${overtimeTaken ? 'inline-block' : 'none'};">
<input type="number"
data-date="${dateStr}"
data-field="overtime_taken_hours"
value="${overtimeTaken}"
min="0"
step="0.25"
placeholder="0.00"
${disabled}
onblur="handleOvertimeChange('${dateStr}', this.value); saveEntry(this);"
oninput="updateOvertimeDisplay();"
onchange="handleOvertimeChange('${dateStr}', this.value); updateOvertimeDisplay();"
style="width: 80px; margin-left: 5px;"
class="overtime-input">
<span>h</span>
</div>
</div>
<div class="vacation-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleVacationSelect('${dateStr}')" style="margin-right: 5px;">
Urlaub
</button>
<div id="vacation-select-${dateStr}" style="display: ${vacationType ? 'inline-block' : 'none'};">
<select data-date="${dateStr}"
data-field="vacation_type"
${disabled}
onchange="saveEntry(this); updateOvertimeDisplay();"
oninput="updateOvertimeDisplay();"
style="margin-left: 5px;"
class="vacation-select">
<option value="">--</option>
<option value="half" ${vacationType === 'half' ? 'selected' : ''}>Halber Tag</option>
<option value="full" ${vacationType === 'full' ? 'selected' : ''}>Ganzer Tag</option>
</select>
</div>
</div>
<div class="sick-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSickStatus('${dateStr}')" style="margin-right: 5px; ${sickStatus ? 'background-color: #e74c3c; color: white;' : ''}">
Krank
</button>
</div>
${isWeekend ? `
<div class="travel-control">
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleWeekendTravel('${dateStr}')" style="margin-right: 5px; ${weekendTravel ? 'background-color: #28a745; color: white;' : ''}">
Reise
</button>
</div>
` : ''}
</div>
</td>
</tr>
`;
}
// Werktage (Montag bis Freitag) rendern
for (let i = 0; i < 5; i++) {
html += renderDay(i);
}
// Wochenende (Samstag und Sonntag) in zusammenklappbarer Sektion
html += `
<tr>
<td colspan="6" style="padding: 0; border: none;">
<div class="weekend-section" style="margin-top: 10px;">
<div class="collapsible-header" onclick="toggleWeekendSection()" style="cursor: pointer; padding: 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0; font-size: 14px; font-weight: 600;">Wochenende</h4>
<span id="weekendToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
</div>
<div id="weekendContent" style="display: none; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
<table style="width: 100%; border-collapse: collapse;">
<tbody>
`;
// Samstag und Sonntag rendern
for (let i = 5; i < 7; i++) {
html += renderDay(i);
}
html += `
</tbody>
</table>
</div>
</div>
</td>
</tr>
`;
html += `
</tbody>
</table>
</div>
`;
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 = '<span style="color: #28a745;">(Urlaub - ganzer Tag)</span>';
if (isFullDayVacation) {
// Entferne zuerst alle Urlaub-Spans falls vorhanden (mit verschiedenen möglichen Formaten)
dateText = dateText.replace(/\s*<span[^>]*>\(Urlaub - ganzer Tag\)<\/span>/gi, '');
// Entferne auch "(Krank)" falls vorhanden
dateText = dateText.replace(/\s*<span[^>]*>\(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*<span[^>]*>\(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 = `
<h3 style="margin-top: 0; margin-bottom: 15px;">Neue Version einreichen</h3>
<p style="margin-bottom: 15px; color: #666;">
Es existiert bereits eine Version für diese Woche. Bitte geben Sie einen Grund an, warum Sie eine neue Version einreichen:
</p>
<textarea
id="versionReasonInput"
placeholder="Grund für die neue Version..."
style="width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; resize: vertical;"
required></textarea>
<div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelReasonBtn" class="btn btn-secondary">Abbrechen</button>
<button id="submitReasonBtn" class="btn btn-success">Einreichen</button>
</div>
`;
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 = '<span style="color: #e74c3c;">(Krank)</span>';
if (newStatus) {
// Prüfe ob bereits vorhanden
if (!dateText.includes('(Krank)')) {
dateText += ' ' + sickSpan;
dateCell.innerHTML = dateText;
}
} else {
// Entferne "(Krank)" Span
dateText = dateText.replace(/ <span style="color: #e74c3c;">\(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');
}
};