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