// ============================================
// STATE & VARIABLES
// ============================================
let currentEditingId = null;
let datePicker = null;
let startTimePicker = null;
let endTimePicker = null;
let filterFromPicker = null;
let filterToPicker = null;
let manualStartTimePicker = null;
// Timer state
let timerInterval = null;
let timerStartTime = null;
let timerPausedDuration = 0; // Total paused time in seconds
let isPaused = false;
let pauseTimeout = null;
let currentEntryId = null; // ID of today's entry being timed
// Current month display state
let displayYear = new Date().getFullYear();
let displayMonth = new Date().getMonth(); // 0-11
// Current view state
let currentView = 'monthly'; // 'monthly' or 'filter'
let currentFilterFrom = null;
let currentFilterTo = null;
// Bulk edit state
let bulkEditMode = false;
let selectedEntries = new Set();
// Settings state
let currentBundesland = 'BW'; // Default: Baden-Württemberg
let totalVacationDays = 30; // Default vacation days per year
// ============================================
// UTILITY FUNCTIONS
// ============================================
/**
* Format date from YYYY-MM-DD to DD.MM.YYYY
*/
function formatDateDisplay(dateStr) {
const [year, month, day] = dateStr.split('-');
return `${day}.${month}.${year}`;
}
/**
* Format date from DD.MM.YYYY to YYYY-MM-DD
*/
function formatDateISO(dateStr) {
const [day, month, year] = dateStr.split('.');
return `${year}-${month}-${day}`;
}
/**
* Get today's date in YYYY-MM-DD format
*/
function getTodayISO() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Round time down to nearest 15 minutes
*/
function roundDownTo15Min(date) {
const minutes = date.getMinutes();
const roundedMinutes = Math.floor(minutes / 15) * 15;
date.setMinutes(roundedMinutes);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}
/**
* Round time up to nearest 15 minutes
*/
function roundUpTo15Min(date) {
const minutes = date.getMinutes();
const roundedMinutes = Math.ceil(minutes / 15) * 15;
date.setMinutes(roundedMinutes);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
}
/**
* Format time as HH:MM
*/
function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Format seconds to HH:MM:SS
*/
function formatDuration(seconds) {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
/**
* Show toast notification
*/
function showNotification(message, type = 'info') {
const container = document.getElementById('toastContainer');
// Create toast element
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
// Icon based on type
const icons = {
success: '✓',
error: '✕',
info: 'ℹ'
};
toast.innerHTML = `
${icons[type] || 'ℹ'}
${message}
`;
container.appendChild(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, 3000);
}
/**
* Get day of week abbreviation in German
*/
function getDayOfWeek(date) {
const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
return days[date.getDay()];
}
/**
* Check if date is weekend
*/
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6; // Sunday or Saturday
}
/**
* Calculate Easter Sunday for a given year (Gauss algorithm)
*/
function getEasterSunday(year) {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return new Date(year, month - 1, day);
}
/**
* Get all public holidays for a given year and Bundesland
*/
function getPublicHolidays(year, bundesland = currentBundesland) {
const holidays = [];
// Fixed holidays (all states)
holidays.push({ date: new Date(year, 0, 1), name: 'Neujahr' });
holidays.push({ date: new Date(year, 4, 1), name: 'Tag der Arbeit' });
holidays.push({ date: new Date(year, 9, 3), name: 'Tag der Deutschen Einheit' });
holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' });
holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' });
// Heilige Drei Könige (BW, BY, ST)
if (['BW', 'BY', 'ST'].includes(bundesland)) {
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
}
// Internationaler Frauentag (BE, MV since 2023)
if (['BE'].includes(bundesland) || (bundesland === 'MV' && year >= 2023)) {
holidays.push({ date: new Date(year, 2, 8), name: 'Internationaler Frauentag' });
}
// Weltkindertag (TH since 2019)
if (bundesland === 'TH' && year >= 2019) {
holidays.push({ date: new Date(year, 8, 20), name: 'Weltkindertag' });
}
// Reformationstag (BB, MV, SN, ST, TH, + HB, HH, NI, SH since 2018)
const reformationstagStates = ['BB', 'MV', 'SN', 'ST', 'TH'];
if (year >= 2018) {
reformationstagStates.push('HB', 'HH', 'NI', 'SH');
}
if (reformationstagStates.includes(bundesland)) {
holidays.push({ date: new Date(year, 9, 31), name: 'Reformationstag' });
}
// Allerheiligen (BW, BY, NW, RP, SL)
if (['BW', 'BY', 'NW', 'RP', 'SL'].includes(bundesland)) {
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
}
// Buß- und Bettag (only SN)
if (bundesland === 'SN') {
// Buß- und Bettag is the Wednesday before November 23
let bussbettag = new Date(year, 10, 23);
while (bussbettag.getDay() !== 3) { // 3 = Wednesday
bussbettag.setDate(bussbettag.getDate() - 1);
}
bussbettag.setDate(bussbettag.getDate() - 7); // One week before
holidays.push({ date: bussbettag, name: 'Buß- und Bettag' });
}
// Easter-dependent holidays
const easter = getEasterSunday(year);
// Karfreitag (Good Friday) - 2 days before Easter (all states)
const goodFriday = new Date(easter);
goodFriday.setDate(easter.getDate() - 2);
holidays.push({ date: goodFriday, name: 'Karfreitag' });
// Ostermontag (Easter Monday) - 1 day after Easter (all states)
const easterMonday = new Date(easter);
easterMonday.setDate(easter.getDate() + 1);
holidays.push({ date: easterMonday, name: 'Ostermontag' });
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter (all states)
const ascension = new Date(easter);
ascension.setDate(easter.getDate() + 39);
holidays.push({ date: ascension, name: 'Christi Himmelfahrt' });
// Pfingstmontag (Whit Monday) - 50 days after Easter (all states)
const whitMonday = new Date(easter);
whitMonday.setDate(easter.getDate() + 50);
holidays.push({ date: whitMonday, name: 'Pfingstmontag' });
// Fronleichnam (Corpus Christi) - 60 days after Easter (BW, BY, HE, NW, RP, SL, + some communities in SN, TH)
if (['BW', 'BY', 'HE', 'NW', 'RP', 'SL'].includes(bundesland)) {
const corpusChristi = new Date(easter);
corpusChristi.setDate(easter.getDate() + 60);
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
}
// Mariä Himmelfahrt (Assumption of Mary) - August 15 (BY in some communities, SL)
if (['SL'].includes(bundesland)) {
holidays.push({ date: new Date(year, 7, 15), name: 'Mariä Himmelfahrt' });
}
return holidays;
}
/**
* Check if date is a public holiday
* Returns the holiday name or null
*/
function getHolidayName(date) {
const year = date.getFullYear();
const holidays = getPublicHolidays(year, currentBundesland);
const dateStr = date.toISOString().split('T')[0];
const holiday = holidays.find(h => {
return h.date.toISOString().split('T')[0] === dateStr;
});
return holiday ? holiday.name : null;
}
/**
* Check if date is a public holiday in Baden-Württemberg
*/
function isPublicHoliday(date) {
return getHolidayName(date) !== null;
}
/**
* Check if date is weekend or public holiday
*/
function isWeekendOrHoliday(date) {
return isWeekend(date) || isPublicHoliday(date);
}
/**
* Get month name in German
*/
function getMonthName(month) {
const months = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
];
return months[month];
}
/**
* Update month display
*/
function updateMonthDisplay() {
const monthName = getMonthName(displayMonth);
const displayText = `${monthName} ${displayYear}`;
document.getElementById('currentMonthDisplay').textContent = displayText;
// Always show next month button (allow navigation to future)
const nextBtn = document.getElementById('btnNextMonth');
nextBtn.style.visibility = 'visible';
}
/**
* Navigate to previous month
*/
function handlePrevMonth() {
if (displayMonth === 0) {
displayMonth = 11;
displayYear--;
} else {
displayMonth--;
}
loadMonthlyView();
}
/**
* Navigate to next month
*/
function handleNextMonth() {
// Allow navigation to future months
if (displayMonth === 11) {
displayMonth = 0;
displayYear++;
} else {
displayMonth++;
}
loadMonthlyView();
}
// ============================================
// TIMER FUNCTIONS
// ============================================
/**
* Check if timer is running (on page load)
*/
async function checkRunningTimer() {
const today = getTodayISO();
const entries = await fetchEntries(today, today);
if (entries.length > 0) {
const todayEntry = entries[0];
// Check if entry has start but same start and end (running timer)
if (todayEntry.startTime === todayEntry.endTime) {
// Timer is running
currentEntryId = todayEntry.id;
// Calculate start time from DB
const [hours, minutes] = todayEntry.startTime.split(':').map(Number);
const startDate = new Date();
startDate.setHours(hours, minutes, 0, 0);
timerStartTime = startDate.getTime();
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
startBtn.disabled = true;
startBtn.classList.add('opacity-50', 'cursor-not-allowed');
startBtn.classList.remove('hover:bg-green-700');
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
document.getElementById('timerStatus').textContent = 'Läuft seit ' + todayEntry.startTime;
document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
document.getElementById('timerStatus').classList.add('cursor-default');
// Start timer interval
timerInterval = setInterval(updateTimer, 1000);
updateTimer(); // Immediate update
// Schedule automatic pauses
const elapsed = Date.now() - timerStartTime;
schedulePausesWithOffset(elapsed);
}
}
}
/**
* Start work timer
*/
async function startWork() {
const now = new Date();
const roundedStart = roundDownTo15Min(new Date(now));
const startTime = formatTime(roundedStart);
const today = getTodayISO();
// Create entry with same start and end time (indicates running)
const entry = await createEntry(formatDateDisplay(today), startTime, startTime, null);
if (!entry) {
return;
}
currentEntryId = entry.id;
timerStartTime = roundedStart.getTime();
timerPausedDuration = 0;
isPaused = false;
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
startBtn.disabled = true;
startBtn.classList.add('opacity-50', 'cursor-not-allowed');
startBtn.classList.remove('hover:bg-green-700');
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
document.getElementById('timerStatus').textContent = 'Läuft seit ' + startTime;
document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
document.getElementById('timerStatus').classList.add('cursor-default');
// Start timer interval
timerInterval = setInterval(updateTimer, 1000);
// Schedule automatic pauses
schedulePausesWithOffset(0);
// Reload view to show entry
loadMonthlyView();
}
/**
* Stop work timer
*/
async function stopWork() {
if (!currentEntryId) {
return;
}
clearInterval(timerInterval);
if (pauseTimeout) clearTimeout(pauseTimeout);
const now = new Date();
const roundedEnd = roundUpTo15Min(new Date(now));
const endTime = formatTime(roundedEnd);
const today = getTodayISO();
// Fetch the entry to get start time
const entries = await fetchEntries(today, today);
const entry = entries.find(e => e.id === currentEntryId);
if (entry) {
// Update entry with end time
await updateEntry(currentEntryId, formatDateDisplay(today), entry.startTime, endTime, null);
}
// Reset timer state
currentEntryId = null;
timerStartTime = null;
timerPausedDuration = 0;
isPaused = false;
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
startBtn.disabled = false;
startBtn.classList.remove('opacity-50', 'cursor-not-allowed');
startBtn.classList.add('hover:bg-green-700');
stopBtn.disabled = true;
stopBtn.classList.add('opacity-50', 'cursor-not-allowed');
stopBtn.classList.remove('hover:bg-red-700');
document.getElementById('timerDisplay').textContent = '00:00:00';
document.getElementById('timerStatus').textContent = 'Nicht gestartet';
document.getElementById('timerStatus').classList.add('cursor-pointer', 'underline', 'hover:text-blue-300');
document.getElementById('timerStatus').classList.remove('cursor-default');
// Reload monthly view
loadMonthlyView();
}
/**
* Update timer display
*/
function updateTimer() {
if (!timerStartTime) return;
const elapsed = Math.floor((Date.now() - timerStartTime) / 1000) - timerPausedDuration;
document.getElementById('timerDisplay').textContent = formatDuration(elapsed);
}
/**
* Schedule automatic pauses at 6h and 9h with offset for existing elapsed time
*/
function schedulePausesWithOffset(elapsedMs) {
const sixHoursMs = 6 * 60 * 60 * 1000;
const nineHoursMs = 9 * 60 * 60 * 1000;
// Pause at 6 hours for 30 minutes
if (elapsedMs < sixHoursMs) {
setTimeout(() => {
if (timerStartTime && !isPaused) {
pauseTimer(30 * 60); // 30 minutes
showNotification('⏸️ Automatische Pause: 30 Minuten (nach 6 Stunden)', 'info');
}
}, sixHoursMs - elapsedMs);
}
// Additional pause at 9 hours for 15 minutes
if (elapsedMs < nineHoursMs) {
setTimeout(() => {
if (timerStartTime && !isPaused) {
pauseTimer(15 * 60); // 15 minutes
showNotification('⏸️ Automatische Pause: 15 Minuten (nach 9 Stunden)', 'info');
}
}, nineHoursMs - elapsedMs);
}
}
/**
* Pause timer for specified duration
*/
function pauseTimer(durationSeconds) {
isPaused = true;
document.getElementById('timerStatus').textContent = `Pause (${Math.floor(durationSeconds / 60)} Min)...`;
pauseTimeout = setTimeout(() => {
timerPausedDuration += durationSeconds;
isPaused = false;
document.getElementById('timerStatus').textContent = 'Läuft...';
}, durationSeconds * 1000);
}
/**
* Fetch entries from the backend
*/
async function fetchEntries(fromDate = null, toDate = null) {
try {
let url = '/api/entries';
const params = new URLSearchParams();
if (fromDate) params.append('from', fromDate);
if (toDate) params.append('to', toDate);
if (params.toString()) {
url += '?' + params.toString();
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch entries');
}
const entries = await response.json();
return entries;
} catch (error) {
console.error('Error fetching entries:', error);
showNotification('Fehler beim Laden der Einträge', 'error');
return [];
}
}
/**
* Create a new entry
*/
async function createEntry(date, startTime, endTime, pauseMinutes, location) {
try {
const body = {
date: formatDateISO(date),
startTime,
endTime,
location: location || 'office'
};
// Only include pauseMinutes if explicitly provided (not empty)
if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') {
body.pauseMinutes = parseInt(pauseMinutes);
}
const response = await fetch('/api/entries', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create entry');
}
const entry = await response.json();
return entry;
} catch (error) {
console.error('Error creating entry:', error);
showNotification(error.message || 'Fehler beim Erstellen des Eintrags', 'error');
return null;
}
}
/**
* Update an existing entry
*/
async function updateEntry(id, date, startTime, endTime, pauseMinutes, location) {
try {
const body = {
date: formatDateISO(date),
startTime,
endTime,
location: location || 'office'
};
// Only include pauseMinutes if explicitly provided (not empty)
if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') {
body.pauseMinutes = parseInt(pauseMinutes);
}
const response = await fetch(`/api/entries/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to update entry');
}
const entry = await response.json();
return entry;
} catch (error) {
console.error('Error updating entry:', error);
showNotification(error.message || 'Fehler beim Aktualisieren des Eintrags', 'error');
return null;
}
}
/**
* Delete an entry
*/
async function deleteEntry(id) {
try {
const response = await fetch(`/api/entries/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete entry');
}
return true;
} catch (error) {
console.error('Error deleting entry:', error);
showNotification('Fehler beim Löschen des Eintrags', 'error');
return false;
}
}
/**
* Get a setting by key
*/
async function getSetting(key) {
try {
const response = await fetch(`/api/settings/${key}`);
if (!response.ok) {
if (response.status === 404) {
return null; // Setting doesn't exist yet
}
throw new Error('Failed to get setting');
}
const data = await response.json();
return data.value;
} catch (error) {
console.error('Error getting setting:', error);
return null;
}
}
/**
* Set a setting
*/
async function setSetting(key, value) {
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key, value })
});
if (!response.ok) {
throw new Error('Failed to set setting');
}
return true;
} catch (error) {
console.error('Error setting setting:', error);
showNotification('Fehler beim Speichern der Einstellung', 'error');
return false;
}
}
/**
* Export entries as CSV
*/
async function exportEntries(fromDate = null, toDate = null) {
try {
let url = '/api/export';
const params = new URLSearchParams();
if (fromDate) params.append('from', fromDate);
if (toDate) params.append('to', toDate);
if (params.toString()) {
url += '?' + params.toString();
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to export entries');
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = 'zeiterfassung.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
showNotification('Export erfolgreich', 'success');
} catch (error) {
console.error('Error exporting entries:', error);
showNotification('Fehler beim Exportieren', 'error');
}
}
// ============================================
// UI FUNCTIONS
// ============================================
/**
* Render entries in the table
*/
function renderEntries(entries) {
const tbody = document.getElementById('entriesTableBody');
const emptyState = document.getElementById('emptyState');
tbody.innerHTML = '';
if (entries.length === 0) {
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
entries.forEach(entry => {
const row = document.createElement('tr');
const dateObj = new Date(entry.date + 'T00:00:00');
const dayOfWeek = getDayOfWeek(dateObj);
const weekend = isWeekendOrHoliday(dateObj);
const location = entry.location || 'office';
// Row color based on location
let rowClass = 'hover:bg-gray-700';
if (location === 'home') {
rowClass = 'hover:bg-green-900 bg-green-950';
} else if (weekend) {
rowClass = 'hover:bg-gray-700 bg-gray-700';
}
row.className = rowClass;
row.dataset.id = entry.id;
// Location icon and text
const locationIcon = location === 'home'
? ''
: '';
const locationText = location === 'home' ? 'Home' : 'Büro';
// Checkbox column (always present for consistent layout)
const checkboxCell = bulkEditMode ? `
|
` : ' | ';
row.innerHTML = checkboxCell + `
${dayOfWeek} |
${formatDateDisplay(entry.date)} |
${entry.startTime}
|
${entry.endTime}
|
${entry.pauseMinutes}
|
${entry.netHours.toFixed(2)} |
${locationIcon} ${locationText}
|
|
`;
tbody.appendChild(row);
});
// Reinitialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Add event listeners
attachInlineEditListeners();
document.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
const id = parseInt(btn.dataset.id);
const entries = await fetchEntries();
const entry = entries.find(e => e.id === id);
if (entry) {
openModal(entry);
}
});
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', () => handleDelete(parseInt(btn.dataset.id)));
});
// Checkbox event listeners for bulk edit
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
toggleEntrySelection(parseInt(checkbox.dataset.id));
});
});
}
/**
* Render monthly view with all days
*/
function renderMonthlyView(entries) {
const tbody = document.getElementById('entriesTableBody');
const emptyState = document.getElementById('emptyState');
tbody.innerHTML = '';
emptyState.classList.add('hidden');
const today = new Date();
const todayYear = today.getFullYear();
const todayMonth = today.getMonth();
const todayDay = today.getDate();
// Show all days of the month
const lastDay = new Date(displayYear, displayMonth + 1, 0).getDate();
// Create a map of entries by date
const entriesMap = {};
entries.forEach(entry => {
entriesMap[entry.date] = entry;
});
// Render all days from 1st to lastDay
for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(displayYear, displayMonth, day);
const dateISO = `${displayYear}-${String(displayMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const entry = entriesMap[dateISO];
const dayOfWeek = getDayOfWeek(dateObj);
const weekend = isWeekendOrHoliday(dateObj);
// Check if this is today
const isToday = dateISO === getTodayISO();
const row = document.createElement('tr');
if (entry) {
// Day has entry
const location = entry.location || 'office';
const entryType = entry.entryType || 'work';
// Row color based on entry type and location
let rowClass = 'hover:bg-gray-700';
if (entryType === 'vacation') {
rowClass = 'hover:bg-yellow-900 bg-yellow-950';
} else if (entryType === 'flextime') {
rowClass = 'hover:bg-cyan-900 bg-cyan-950';
} else if (location === 'home') {
rowClass = 'hover:bg-green-900 bg-green-950';
} else if (weekend) {
rowClass = 'hover:bg-gray-700 bg-gray-700';
}
row.className = rowClass;
row.dataset.id = entry.id;
// Add inline border style for today
if (isToday) {
row.style.borderLeft = '4px solid #3b82f6'; // blue-500
}
// Icon and text based on entry type
let displayIcon, displayText, displayTimes;
if (entryType === 'vacation') {
displayIcon = '';
displayText = 'Urlaub';
displayTimes = `
Urlaub
| `;
} else if (entryType === 'flextime') {
displayIcon = '';
displayText = 'Gleitzeit';
displayTimes = `
Gleittag (8h)
| `;
} else {
displayIcon = location === 'home'
? ''
: '';
displayText = location === 'home' ? 'Home' : 'Büro';
// Check if timer is running (start == end time)
const isTimerRunning = entry.startTime === entry.endTime;
const endTimeDisplay = isTimerRunning
? ''
: entry.endTime;
displayTimes = `
${entry.startTime}
|
${endTimeDisplay}
|
${entry.pauseMinutes}
| `;
}
// Checkbox column (always present for consistent layout)
const checkboxCell = bulkEditMode ? `
|
` : ' | ';
row.innerHTML = checkboxCell + `
${dayOfWeek} |
${formatDateDisplay(entry.date)} |
${displayTimes}
${entry.netHours.toFixed(2)} |
${displayIcon} ${displayText}
|
${entryType === 'work' ? `
` : ''}
|
`;
} else {
// Day has no entry - show add options
const holidayName = getHolidayName(dateObj);
const displayText = holidayName || 'Kein Eintrag';
let emptyRowClass = weekend ? 'hover:bg-gray-700 bg-gray-700' : 'hover:bg-gray-700 bg-red-950/40';
row.className = emptyRowClass;
row.dataset.date = dateISO; // Store date for bulk operations
// Add inline border style for today
if (isToday) {
row.style.borderLeft = '4px solid #3b82f6'; // blue-500
}
// Checkbox cell for empty days (in bulk edit mode)
const checkboxCell = bulkEditMode ? `
|
` : ' | ';
const colspan = bulkEditMode ? '5' : '5';
row.innerHTML = checkboxCell + `
${dayOfWeek} |
${formatDateDisplay(dateISO)} |
${displayText}
|
${!weekend ? `
` : ''}
|
`;
}
tbody.appendChild(row);
}
// Reinitialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Add event listeners
attachInlineEditListeners();
document.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
const id = parseInt(btn.dataset.id);
const entries = await fetchEntries();
const entry = entries.find(e => e.id === id);
if (entry) {
openModal(entry);
}
});
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', () => handleDelete(parseInt(btn.dataset.id)));
});
// Add work entry for a specific date
document.querySelectorAll('.btn-add-work').forEach(btn => {
btn.addEventListener('click', () => {
const dateISO = btn.dataset.date;
openModalForDate(dateISO);
});
});
// Add vacation entry for a specific date
document.querySelectorAll('.btn-add-vacation').forEach(btn => {
btn.addEventListener('click', async () => {
const dateISO = btn.dataset.date;
await addSpecialEntry(dateISO, 'vacation');
});
});
// Add flextime entry for a specific date
document.querySelectorAll('.btn-add-flextime').forEach(btn => {
btn.addEventListener('click', async () => {
const dateISO = btn.dataset.date;
await addSpecialEntry(dateISO, 'flextime');
});
});
// Checkbox event listeners for bulk edit (existing entries)
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
toggleEntrySelection(parseInt(checkbox.dataset.id));
});
});
// Checkbox event listeners for bulk edit (empty days)
document.querySelectorAll('.empty-day-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
toggleEmptyDaySelection(checkbox.dataset.date);
});
});
}
/**
* Add a special entry (vacation or flextime) for a specific date
*/
async function addSpecialEntry(dateISO, entryType) {
const typeName = entryType === 'vacation' ? 'Urlaub' : 'Gleittag';
if (!confirm(`${typeName} für ${formatDateDisplay(dateISO)} eintragen?`)) {
return;
}
try {
const response = await fetch('/api/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: dateISO,
entryType: entryType
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Fehler beim Erstellen');
}
const message = entryType === 'vacation' ? '✅ Urlaub eingetragen (kein Arbeitstag)' : '✅ Gleittag eingetragen (8h Soll, 0h Ist)';
showNotification(message, 'success');
loadMonthlyView();
} catch (error) {
showNotification(error.message, 'error');
}
}
/**
* Load and display entries
*/
async function loadEntries(fromDate = null, toDate = null) {
const entries = await fetchEntries(fromDate, toDate);
renderEntries(entries);
}
/**
* Load and display monthly view
*/
async function loadMonthlyView() {
currentView = 'monthly';
showMonthNavigation();
// First day of display month
const fromDate = `${displayYear}-${String(displayMonth + 1).padStart(2, '0')}-01`;
// Last day of display month
const lastDay = new Date(displayYear, displayMonth + 1, 0).getDate();
const toDate = `${displayYear}-${String(displayMonth + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
const entries = await fetchEntries(fromDate, toDate);
renderMonthlyView(entries);
updateMonthDisplay();
updateStatistics(entries);
}
/**
* Reload the current view (monthly or filter)
*/
async function reloadView() {
if (currentView === 'filter') {
const entries = await fetchEntries(currentFilterFrom, currentFilterTo);
renderEntries(entries);
} else {
await loadMonthlyView();
}
}
/**
* Show month navigation controls
*/
function showMonthNavigation() {
const nav = document.getElementById('monthNavigation');
if (nav) nav.classList.remove('hidden');
}
/**
* Hide month navigation controls
*/
function hideMonthNavigation() {
const nav = document.getElementById('monthNavigation');
if (nav) nav.classList.add('hidden');
}
/**
* Calculate and update statistics for the current month
*/
async function updateStatistics(entries) {
const today = new Date();
const currentYear = displayYear;
const currentMonth = displayMonth;
const lastDay = new Date(currentYear, currentMonth + 1, 0).getDate();
// Count workdays (excluding weekends and holidays, only up to today)
let workdaysCount = 0;
let workdaysPassed = 0;
let totalWorkdaysInMonth = 0; // For statistics display
// Count vacation days to exclude from workdays
const vacationDays = new Set(
entries
.filter(e => e.entryType === 'vacation')
.map(e => e.date)
);
// Count flextime days (they are workdays with 0 hours worked)
const flextimeDays = new Set(
entries
.filter(e => e.entryType === 'flextime')
.map(e => e.date)
);
for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(currentYear, currentMonth, day);
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const dayStr = String(dateObj.getDate()).padStart(2, '0');
const dateISO = `${year}-${month}-${dayStr}`;
const isVacation = vacationDays.has(dateISO);
const isFlextime = flextimeDays.has(dateISO);
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
if (!isWeekendHoliday && !isVacation) {
// Normal workday (excluding vacation days)
totalWorkdaysInMonth++;
workdaysCount++;
if (dateObj <= today) {
workdaysPassed++;
}
} else if (isFlextime && isWeekendHoliday) {
// Flextime on weekend/holiday counts as additional workday
totalWorkdaysInMonth++;
if (new Date(dateISO) <= today) {
workdaysPassed++;
}
}
// Vacation days are excluded from all counts
}
// Calculate target hours (8h per workday passed)
const targetHours = workdaysPassed * 8;
// Calculate actual hours worked (only up to today)
let actualHours = entries
.filter(e => new Date(e.date) <= today)
.reduce((sum, entry) => sum + entry.netHours, 0);
// Add currently running timer hours to actual hours
if (timerStartTime) {
const now = new Date();
const elapsedMs = now.getTime() - timerStartTime;
const elapsedHours = elapsedMs / (1000 * 60 * 60);
actualHours += elapsedHours;
}
// Calculate balance for current month
const balance = actualHours - targetHours;
// Calculate previous month balance
const previousBalance = await calculatePreviousBalance();
// Total balance = previous balance + current month balance
const totalBalance = previousBalance + balance;
// Update UI
document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h';
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
document.getElementById('statWorkdays').textContent = `${entries.length}/${totalWorkdaysInMonth}`;
// Current month balance
const balanceElement = document.getElementById('statBalance');
balanceElement.textContent = (balance >= 0 ? '+' : '') + balance.toFixed(1) + 'h';
if (balance > 0) {
balanceElement.className = 'text-2xl font-bold text-green-400';
} else if (balance < 0) {
balanceElement.className = 'text-2xl font-bold text-red-400';
} else {
balanceElement.className = 'text-2xl font-bold text-gray-100';
}
// Previous month balance
const prevBalanceElement = document.getElementById('statPreviousBalance');
prevBalanceElement.textContent = (previousBalance >= 0 ? '+' : '') + previousBalance.toFixed(1) + 'h';
// Total balance
const totalBalanceElement = document.getElementById('statTotalBalance');
totalBalanceElement.textContent = (totalBalance >= 0 ? '+' : '') + totalBalance.toFixed(1) + 'h';
if (totalBalance > 0) {
totalBalanceElement.className = 'text-3xl font-bold text-green-400';
} else if (totalBalance < 0) {
totalBalanceElement.className = 'text-3xl font-bold text-red-400';
} else {
totalBalanceElement.className = 'text-3xl font-bold text-gray-100';
}
// Update vacation statistics
await updateVacationStatistics();
}
/**
* Update vacation statistics for the year
*/
async function updateVacationStatistics() {
const currentYear = displayYear; // Use displayed year, not current calendar year
const today = new Date();
// Fetch all entries for the displayed year
const fromDate = `${currentYear}-01-01`;
const toDate = `${currentYear}-12-31`;
const allEntries = await fetchEntries(fromDate, toDate);
// Filter vacation entries
const vacationEntries = allEntries.filter(e => e.entryType === 'vacation');
// Calculate taken (past and today) and planned (future) vacation days
const vacationTaken = vacationEntries.filter(e => new Date(e.date) <= today).length;
const vacationPlanned = vacationEntries.filter(e => new Date(e.date) > today).length;
const vacationRemaining = totalVacationDays - vacationTaken - vacationPlanned;
// Update UI with dynamic year
const vacationLabel = document.getElementById('vacationYearLabel');
vacationLabel.innerHTML = ` Urlaub ${currentYear}`;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
document.getElementById('statVacationTaken').textContent = vacationTaken;
document.getElementById('statVacationPlanned').textContent = vacationPlanned;
document.getElementById('statVacationRemaining').textContent = `${vacationRemaining} / ${totalVacationDays}`;
}
/**
* Calculate balance from all previous months (starting from first month with entries)
*/
async function calculatePreviousBalance() {
const currentYear = displayYear;
const currentMonth = displayMonth;
// Find the first month with any entries by checking all entries
const allEntries = await fetchEntries();
if (allEntries.length === 0) {
return 0;
}
// Find earliest date
const earliestDate = allEntries.reduce((earliest, entry) => {
const entryDate = new Date(entry.date);
return !earliest || entryDate < earliest ? entryDate : earliest;
}, null);
if (!earliestDate) {
return 0;
}
const firstYear = earliestDate.getFullYear();
const firstMonth = earliestDate.getMonth();
// Calculate balance from first month to previous month (not including current displayed month)
let totalBalance = 0;
let checkYear = firstYear;
let checkMonth = firstMonth;
const today = new Date();
// Loop through all months from first entry until previous month of displayed month
while (checkYear < currentYear || (checkYear === currentYear && checkMonth < currentMonth)) {
const firstDay = `${checkYear}-${String(checkMonth + 1).padStart(2, '0')}-01`;
const lastDay = new Date(checkYear, checkMonth + 1, 0).getDate();
const lastDayStr = `${checkYear}-${String(checkMonth + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
const entries = await fetchEntries(firstDay, lastDayStr);
// For past months, use full month. For current month (if displayed), limit to today
const monthEnd = new Date(checkYear, checkMonth + 1, 0);
const limitDate = monthEnd; // Always use full month for previous balance calculation
let workdaysPassed = 0;
const monthLastDay = new Date(checkYear, checkMonth + 1, 0).getDate();
// Count vacation days to exclude from workdays
const vacationDays = new Set(
entries
.filter(e => e.entryType === 'vacation')
.map(e => e.date)
);
// Count flextime days (they are workdays with 0 hours worked)
const flextimeDays = new Set(
entries
.filter(e => e.entryType === 'flextime')
.map(e => e.date)
);
for (let day = 1; day <= monthLastDay; day++) {
const dateObj = new Date(checkYear, checkMonth, day);
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const dayStr = String(dateObj.getDate()).padStart(2, '0');
const dateISO = `${year}-${month}-${dayStr}`;
if (!isWeekendOrHoliday(dateObj)) {
// Exclude vacation days from workdays count
if (!vacationDays.has(dateISO)) {
workdaysPassed++;
}
} else if (flextimeDays.has(dateISO)) {
// Flextime on weekend/holiday counts as workday
workdaysPassed++;
}
}
const targetHours = workdaysPassed * 8;
const actualHours = entries.reduce((sum, entry) => sum + entry.netHours, 0);
const monthBalance = actualHours - targetHours;
totalBalance += monthBalance;
// Move to next month
checkMonth++;
if (checkMonth > 11) {
checkMonth = 0;
checkYear++;
}
}
return totalBalance;
}
/**
* Open modal for adding/editing entry
*/
function openModal(entry = null) {
const modal = document.getElementById('entryModal');
const modalTitle = document.getElementById('modalTitle');
const dateInput = document.getElementById('modalDate');
const startTimeInput = document.getElementById('modalStartTime');
const endTimeInput = document.getElementById('modalEndTime');
const pauseInput = document.getElementById('modalPause');
const locationInput = document.getElementById('modalLocation');
if (entry) {
// Edit mode
modalTitle.textContent = 'Eintrag bearbeiten';
currentEditingId = entry.id;
dateInput.value = formatDateDisplay(entry.date);
startTimeInput.value = entry.startTime;
endTimeInput.value = entry.endTime;
pauseInput.value = entry.pauseMinutes;
locationInput.value = entry.location || 'office';
updateLocationButtons(entry.location || 'office');
} else {
// Add mode
modalTitle.textContent = 'Neuer Eintrag';
currentEditingId = null;
dateInput.value = '';
startTimeInput.value = '';
endTimeInput.value = '';
pauseInput.value = '';
locationInput.value = 'office';
updateLocationButtons('office');
}
modal.classList.remove('hidden');
}
/**
* Open modal with pre-filled date
*/
function openModalForDate(dateISO) {
const modal = document.getElementById('entryModal');
const modalTitle = document.getElementById('modalTitle');
const dateInput = document.getElementById('modalDate');
const startTimeInput = document.getElementById('modalStartTime');
const endTimeInput = document.getElementById('modalEndTime');
const pauseInput = document.getElementById('modalPause');
const locationInput = document.getElementById('modalLocation');
modalTitle.textContent = 'Neuer Eintrag';
currentEditingId = null;
dateInput.value = formatDateDisplay(dateISO);
startTimeInput.value = '09:00';
endTimeInput.value = '17:30';
pauseInput.value = '';
locationInput.value = 'office';
updateLocationButtons('office');
modal.classList.remove('hidden');
}
/**
* Update location button states
*/
function updateLocationButtons(location) {
const officeBtn = document.getElementById('btnLocationOffice');
const homeBtn = document.getElementById('btnLocationHome');
if (location === 'home') {
officeBtn.className = 'flex-1 px-4 py-3 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium flex items-center justify-center gap-2';
homeBtn.className = 'flex-1 px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-all duration-200 font-medium flex items-center justify-center gap-2 shadow-sm';
} else {
officeBtn.className = 'flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200 font-medium flex items-center justify-center gap-2 shadow-sm';
homeBtn.className = 'flex-1 px-4 py-3 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium flex items-center justify-center gap-2';
}
// Reinitialize Lucide icons after DOM update
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
/**
* Close modal
*/
function closeModal() {
const modal = document.getElementById('entryModal');
modal.classList.add('hidden');
currentEditingId = null;
}
/**
* Handle form submission
*/
async function handleFormSubmit(e) {
e.preventDefault();
const date = document.getElementById('modalDate').value;
const startTime = document.getElementById('modalStartTime').value;
const endTime = document.getElementById('modalEndTime').value;
const pauseInput = document.getElementById('modalPause').value;
const location = document.getElementById('modalLocation').value;
// Convert empty string or "0" to null for auto-calculation
const pauseMinutes = (pauseInput === '' || pauseInput === '0' || parseInt(pauseInput) === 0) ? null : parseInt(pauseInput);
if (!date || !startTime || !endTime) {
showNotification('Bitte alle Felder ausfüllen', 'error');
return;
}
if (currentEditingId) {
// Update existing entry
const success = await updateEntry(currentEditingId, date, startTime, endTime, pauseMinutes, location);
if (success) {
showNotification('Eintrag aktualisiert', 'success');
closeModal();
loadMonthlyView();
}
} else {
// Create new entry
const success = await createEntry(date, startTime, endTime, pauseMinutes, location);
if (success) {
showNotification('Eintrag erstellt', 'success');
closeModal();
loadMonthlyView();
}
}
}
/**
* Handle delete button click
*/
async function handleDelete(id) {
if (confirm('Möchten Sie diesen Eintrag wirklich löschen?')) {
const success = await deleteEntry(id);
if (success) {
showNotification('Eintrag gelöscht', 'success');
loadMonthlyView();
}
}
}
/**
* Handle quick time button click
* Calculates end time based on start time and target net hours
*/
function handleQuickTimeButton(netHours) {
const startTimeInput = document.getElementById('modalStartTime');
const endTimeInput = document.getElementById('modalEndTime');
// Get start time (default to 8:30 if empty)
let startTime = startTimeInput.value || '08:30';
startTimeInput.value = startTime;
// Parse start time
const [startHour, startMin] = startTime.split(':').map(Number);
const startMinutes = startHour * 60 + startMin;
// Calculate required gross hours including pause
let pauseMinutes = 0;
let grossHours = netHours;
if (netHours >= 9) {
pauseMinutes = 45;
grossHours = netHours + (pauseMinutes / 60);
} else if (netHours >= 6) {
pauseMinutes = 30;
grossHours = netHours + (pauseMinutes / 60);
}
// Calculate end time
const endMinutes = startMinutes + (grossHours * 60);
const endHour = Math.floor(endMinutes / 60);
const endMin = Math.round(endMinutes % 60);
const endTime = `${String(endHour).padStart(2, '0')}:${String(endMin).padStart(2, '0')}`;
endTimeInput.value = endTime;
}
/**
* Auto-fill missing entries for current month with 8h net (9:00-17:30)
*/
async function handleAutoFillMonth() {
if (!confirm('Möchten Sie alle fehlenden Arbeitstage im aktuellen Monat mit 8h (9:00-17:30) ausfüllen?\n\nWochenenden und Feiertage werden übersprungen.')) {
return;
}
const today = new Date();
const currentYear = displayYear;
const currentMonth = displayMonth;
// Get last day of current month
const lastDay = new Date(currentYear, currentMonth + 1, 0).getDate();
// Get existing entries for the month
const fromDate = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-01`;
const toDate = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
const existingEntries = await fetchEntries(fromDate, toDate);
// Create a set of dates that already have entries
const existingDates = new Set(existingEntries.map(e => e.date));
let created = 0;
let skipped = 0;
// Iterate through all days in the month
for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(currentYear, currentMonth, day);
const dateISO = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
// Skip if entry already exists
if (existingDates.has(dateISO)) {
continue;
}
// Skip weekends and holidays
if (isWeekendOrHoliday(dateObj)) {
skipped++;
continue;
}
// Skip future dates
if (dateObj > today) {
skipped++;
continue;
}
// Create entry: 9:00-17:30 (8h net with 30min pause)
const success = await createEntry(formatDateDisplay(dateISO), '09:00', '17:30', null);
if (success) {
created++;
}
}
// Reload view
await reloadView();
showNotification(`✓ ${created} Einträge erstellt, ${skipped} Tage übersprungen`, 'success');
}
// ============================================
// BULK EDIT
// ============================================
/**
* Toggle bulk edit mode
*/
function toggleBulkEditMode() {
bulkEditMode = !bulkEditMode;
selectedEntries.clear();
const bulkEditBar = document.getElementById('bulkEditBar');
const checkboxHeader = document.getElementById('checkboxHeader');
const toggleBtn = document.getElementById('btnToggleBulkEdit');
if (bulkEditMode) {
bulkEditBar.classList.remove('hidden');
checkboxHeader.classList.remove('hidden');
toggleBtn.className = 'inline-flex items-center gap-2 px-4 py-2.5 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-all duration-200 font-medium shadow-sm hover:shadow';
toggleBtn.title = 'Mehrfachauswahl deaktivieren';
} else {
bulkEditBar.classList.add('hidden');
checkboxHeader.classList.add('hidden');
toggleBtn.className = 'inline-flex items-center gap-2 px-4 py-2.5 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium shadow-sm hover:shadow';
toggleBtn.title = 'Mehrfachauswahl aktivieren';
}
// Reload view to show/hide checkboxes
reloadView();
}
/**
* Toggle entry selection
*/
function toggleEntrySelection(id) {
if (selectedEntries.has(id)) {
selectedEntries.delete(id);
} else {
selectedEntries.add(id);
}
updateSelectedCount();
updateCheckboxes();
}
/**
* Toggle empty day selection (for dates without entries)
*/
function toggleEmptyDaySelection(date) {
if (selectedEntries.has(date)) {
selectedEntries.delete(date);
} else {
selectedEntries.add(date);
}
updateSelectedCount();
updateCheckboxes();
}
/**
* Select all visible entries and empty days
*/
function selectAllEntries() {
// Select existing entries
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
const id = parseInt(checkbox.dataset.id);
selectedEntries.add(id);
checkbox.checked = true;
});
// Select empty days
document.querySelectorAll('.empty-day-checkbox').forEach(checkbox => {
const date = checkbox.dataset.date;
selectedEntries.add(date);
checkbox.checked = true;
});
updateSelectedCount();
}
/**
* Deselect all entries
*/
function deselectAllEntries() {
selectedEntries.clear();
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
document.querySelectorAll('.empty-day-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
updateSelectedCount();
}
/**
* Update selected count display
*/
function updateSelectedCount() {
document.getElementById('selectedCount').textContent = `${selectedEntries.size} ausgewählt`;
// Update master checkbox state
const masterCheckbox = document.getElementById('masterCheckbox');
const allCheckboxes = document.querySelectorAll('.entry-checkbox, .empty-day-checkbox');
if (allCheckboxes.length === 0) {
masterCheckbox.checked = false;
masterCheckbox.indeterminate = false;
} else if (selectedEntries.size === allCheckboxes.length) {
masterCheckbox.checked = true;
masterCheckbox.indeterminate = false;
} else if (selectedEntries.size > 0) {
masterCheckbox.checked = false;
masterCheckbox.indeterminate = true;
} else {
masterCheckbox.checked = false;
masterCheckbox.indeterminate = false;
}
}
/**
* Update checkbox states
*/
function updateCheckboxes() {
// Update entry checkboxes (ID-based)
document.querySelectorAll('.entry-checkbox').forEach(checkbox => {
const id = parseInt(checkbox.dataset.id);
checkbox.checked = selectedEntries.has(id);
});
// Update empty day checkboxes (date-based)
document.querySelectorAll('.empty-day-checkbox').forEach(checkbox => {
const date = checkbox.dataset.date;
checkbox.checked = selectedEntries.has(date);
});
updateSelectedCount();
}
/**
* Handle master checkbox toggle
*/
function handleMasterCheckboxToggle() {
const masterCheckbox = document.getElementById('masterCheckbox');
if (masterCheckbox.checked) {
selectAllEntries();
} else {
deselectAllEntries();
}
}
/**
* Bulk set location to office
*/
async function bulkSetLocation(location) {
if (selectedEntries.size === 0) {
showNotification('Keine Einträge ausgewählt', 'error');
return;
}
const locationText = location === 'home' ? 'Home Office' : 'Präsenz';
if (!confirm(`Möchten Sie ${selectedEntries.size} Eintrag/Einträge auf "${locationText}" setzen?`)) {
return;
}
let updated = 0;
const entries = await fetchEntries();
for (const id of selectedEntries) {
const entry = entries.find(e => e.id === id);
if (entry) {
// Convert date from YYYY-MM-DD to DD.MM.YYYY for updateEntry
const formattedDate = formatDateDisplay(entry.date);
const success = await updateEntry(id, formattedDate, entry.startTime, entry.endTime, entry.pauseMinutes, location);
if (success) {
updated++;
}
}
}
selectedEntries.clear();
updateSelectedCount(); // Update counter to 0
await reloadView();
showNotification(`✓ ${updated} Eintrag/Einträge aktualisiert`, 'success');
toggleBulkEditMode(); // Close bulk edit mode
}
/**
* Bulk delete entries
*/
async function bulkDeleteEntries() {
if (selectedEntries.size === 0) {
showNotification('Keine Einträge ausgewählt', 'error');
return;
}
if (!confirm(`Möchten Sie wirklich ${selectedEntries.size} Eintrag/Einträge löschen?`)) {
return;
}
let deleted = 0;
for (const id of selectedEntries) {
const success = await deleteEntry(id);
if (success) {
deleted++;
}
}
selectedEntries.clear();
updateSelectedCount(); // Update counter to 0
await reloadView();
showNotification(`${deleted} Eintrag/Einträge gelöscht`, 'success');
toggleBulkEditMode(); // Close bulk edit mode
}
/**
* Bulk set vacation entries
*/
async function bulkSetVacation() {
if (selectedEntries.size === 0) {
showNotification('Keine Einträge ausgewählt', 'error');
return;
}
if (!confirm(`Möchten Sie ${selectedEntries.size} Eintrag/Einträge als Urlaub eintragen?`)) {
return;
}
let created = 0;
let updated = 0;
let skipped = 0;
const entries = await fetchEntries();
for (const item of selectedEntries) {
// Check if it's an ID (number) or date (string)
if (typeof item === 'number') {
// Update existing entry to vacation
const entry = entries.find(e => e.id === item);
if (entry) {
// Skip weekends/holidays
const dateObj = new Date(entry.date);
if (isWeekendOrHoliday(dateObj)) {
skipped++;
continue;
}
try {
const response = await fetch(`/api/entries/${item}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: entry.date,
entryType: 'vacation'
})
});
if (response.ok) {
updated++;
}
} catch (error) {
console.error('Error updating entry:', error);
}
}
} else {
// Create new vacation entry for empty day (item is date string)
// Skip weekends/holidays
const dateObj = new Date(item);
if (isWeekendOrHoliday(dateObj)) {
skipped++;
continue;
}
try {
const response = await fetch('/api/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: item,
entryType: 'vacation'
})
});
if (response.ok) {
created++;
}
} catch (error) {
console.error('Error creating vacation entry:', error);
}
}
}
selectedEntries.clear();
updateSelectedCount(); // Update counter to 0
await reloadView();
const message = skipped > 0
? `✓ ${created} Urlaub neu eingetragen, ${updated} Einträge geändert, ${skipped} Wochenenden/Feiertage übersprungen`
: `✓ ${created} Urlaub neu eingetragen, ${updated} Einträge geändert`;
showNotification(message, 'success');
toggleBulkEditMode(); // Close bulk edit mode
}
/**
* Bulk set flextime entries
*/
async function bulkSetFlextime() {
if (selectedEntries.size === 0) {
showNotification('Keine Einträge ausgewählt', 'error');
return;
}
if (!confirm(`Möchten Sie ${selectedEntries.size} Eintrag/Einträge als Gleitzeit eintragen?`)) {
return;
}
let created = 0;
let updated = 0;
let skipped = 0;
const entries = await fetchEntries();
for (const item of selectedEntries) {
// Check if it's an ID (number) or date (string)
if (typeof item === 'number') {
// Update existing entry to flextime
const entry = entries.find(e => e.id === item);
if (entry) {
// Skip weekends/holidays
const dateObj = new Date(entry.date);
if (isWeekendOrHoliday(dateObj)) {
skipped++;
continue;
}
try {
const response = await fetch(`/api/entries/${item}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: entry.date,
entryType: 'flextime'
})
});
if (response.ok) {
updated++;
}
} catch (error) {
console.error('Error updating entry:', error);
}
}
} else {
// Create new flextime entry for empty day (item is date string)
// Skip weekends/holidays
const dateObj = new Date(item);
if (isWeekendOrHoliday(dateObj)) {
skipped++;
continue;
}
try {
const response = await fetch('/api/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: item,
entryType: 'flextime'
})
});
if (response.ok) {
created++;
}
} catch (error) {
console.error('Error creating flextime entry:', error);
}
}
}
selectedEntries.clear();
updateSelectedCount(); // Update counter to 0
await reloadView();
const message = skipped > 0
? `✓ ${created} Gleitzeit neu eingetragen, ${updated} Einträge geändert, ${skipped} Wochenenden/Feiertage übersprungen`
: `✓ ${created} Gleitzeit neu eingetragen, ${updated} Einträge geändert`;
showNotification(message, 'success');
toggleBulkEditMode(); // Close bulk edit mode
}
// ============================================
// BUNDESLAND / SETTINGS
// ============================================
/**
* Handle Bundesland change
*/
async function handleBundeslandChange(event) {
const newBundesland = event.target.value;
const oldBundesland = currentBundesland;
// Check for conflicts with existing entries
const entries = await fetchEntries();
const conflicts = [];
// Get old and new holidays for comparison
const currentYear = new Date().getFullYear();
const years = [currentYear - 1, currentYear, currentYear + 1];
const oldHolidays = new Set();
const newHolidays = new Set();
years.forEach(year => {
getPublicHolidays(year, oldBundesland).forEach(h => {
oldHolidays.add(h.date.toISOString().split('T')[0]);
});
getPublicHolidays(year, newBundesland).forEach(h => {
newHolidays.add(h.date.toISOString().split('T')[0]);
});
});
// Find dates that are holidays in new state but not in old state, and have entries
entries.forEach(entry => {
if (newHolidays.has(entry.date) && !oldHolidays.has(entry.date)) {
const dateObj = new Date(entry.date);
// Temporarily set to new bundesland to get holiday name
const tempBundesland = currentBundesland;
currentBundesland = newBundesland;
const holidayName = getHolidayName(dateObj);
currentBundesland = tempBundesland;
conflicts.push({
date: entry.date,
displayDate: formatDateDisplay(entry.date),
holidayName: holidayName
});
}
});
// Warn user if conflicts exist
if (conflicts.length > 0) {
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
const message = `Achtung!\n\nDie folgenden Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nMöchten Sie fortfahren? Die Einträge bleiben erhalten, aber die Tage werden als Feiertage markiert.`;
if (!confirm(message)) {
// Revert selection
event.target.value = oldBundesland;
return;
}
}
// Update state and save
currentBundesland = newBundesland;
await setSetting('bundesland', newBundesland);
// Reload view to show updated holidays
await reloadView();
const bundeslandNames = {
'BW': 'Baden-Württemberg',
'BY': 'Bayern',
'BE': 'Berlin',
'BB': 'Brandenburg',
'HB': 'Bremen',
'HH': 'Hamburg',
'HE': 'Hessen',
'MV': 'Mecklenburg-Vorpommern',
'NI': 'Niedersachsen',
'NW': 'Nordrhein-Westfalen',
'RP': 'Rheinland-Pfalz',
'SL': 'Saarland',
'SN': 'Sachsen',
'ST': 'Sachsen-Anhalt',
'SH': 'Schleswig-Holstein',
'TH': 'Thüringen'
};
showNotification(`✓ Bundesland auf ${bundeslandNames[newBundesland]} gesetzt`, 'success');
}
/**
* Load saved settings
*/
async function loadSettings() {
const savedBundesland = await getSetting('bundesland');
if (savedBundesland) {
currentBundesland = savedBundesland;
document.getElementById('bundeslandSelect').value = savedBundesland;
}
const savedVacationDays = await getSetting('vacationDays');
if (savedVacationDays) {
totalVacationDays = parseInt(savedVacationDays);
document.getElementById('vacationDaysInput').value = totalVacationDays;
}
}
/**
* Handle vacation days input change
*/
async function handleVacationDaysChange(event) {
const newValue = parseInt(event.target.value);
if (isNaN(newValue) || newValue < 0 || newValue > 50) {
showNotification('Bitte geben Sie eine gültige Anzahl (0-50) ein', 'error');
event.target.value = totalVacationDays;
return;
}
totalVacationDays = newValue;
await setSetting('vacationDays', newValue.toString());
await updateVacationStatistics();
showNotification(`✓ Urlaubstage auf ${newValue} pro Jahr gesetzt`, 'success');
}
// ============================================
// INLINE EDITING
// ============================================
/**
* Attach inline edit listeners to all editable cells
*/
function attachInlineEditListeners() {
document.querySelectorAll('.editable-cell').forEach(cell => {
cell.addEventListener('click', handleCellClick);
});
}
/**
* Handle cell click for inline editing
*/
function handleCellClick(e) {
const cell = e.target;
// Prevent editing if already editing
if (cell.classList.contains('editing')) {
return;
}
const field = cell.dataset.field;
const id = parseInt(cell.dataset.id);
const currentValue = cell.dataset.value;
// Create input
cell.classList.add('editing');
const originalContent = cell.innerHTML;
let input;
if (field === 'pauseMinutes') {
input = document.createElement('input');
input.type = 'number';
input.min = '0';
input.step = '1';
input.className = 'cell-input';
input.value = currentValue;
} else {
// Time fields
input = document.createElement('input');
input.type = 'text';
input.className = 'cell-input';
input.value = currentValue;
input.placeholder = 'HH:MM';
}
cell.innerHTML = '';
cell.appendChild(input);
input.focus();
input.select();
// Save on blur or Enter
const saveEdit = async () => {
const newValue = input.value.trim();
if (newValue === currentValue) {
// No change
cell.classList.remove('editing');
cell.innerHTML = originalContent;
return;
}
// Validate
if (field !== 'pauseMinutes') {
// Validate time format
if (!/^\d{1,2}:\d{2}$/.test(newValue)) {
showNotification('Ungültiges Format. Bitte HH:MM verwenden.', 'error');
cell.classList.remove('editing');
cell.innerHTML = originalContent;
return;
}
}
// Get current entry data
const row = cell.closest('tr');
const entryId = parseInt(row.dataset.id);
// Find date from row (skip weekday column, get actual date column)
const dateCells = row.querySelectorAll('td');
const dateText = dateCells[1].textContent; // Second column is date
// Get all values from row
const cells = row.querySelectorAll('.editable-cell');
const values = {};
cells.forEach(c => {
const f = c.dataset.field;
if (c === cell) {
values[f] = newValue;
} else {
values[f] = c.dataset.value;
}
});
// If startTime or endTime changed, set pauseMinutes to null for auto-calculation
// If pauseMinutes was edited, keep the manual value
let pauseToSend = values.pauseMinutes;
if (field === 'startTime' || field === 'endTime') {
pauseToSend = null; // Trigger auto-calculation on server
}
// Update via API
const success = await updateEntry(
entryId,
dateText,
values.startTime,
values.endTime,
pauseToSend
);
if (success) {
// Update cell
cell.dataset.value = newValue;
cell.classList.remove('editing');
cell.textContent = newValue;
// Reload to update calculated values
loadMonthlyView();
} else {
// Revert on error
cell.classList.remove('editing');
cell.innerHTML = originalContent;
}
};
input.addEventListener('blur', saveEdit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveEdit();
} else if (e.key === 'Escape') {
cell.classList.remove('editing');
cell.innerHTML = originalContent;
}
});
}
/**
* Handle filter button click
*/
function handleFilter() {
const fromValue = document.getElementById('filterFrom').value;
const toValue = document.getElementById('filterTo').value;
const fromDate = fromValue ? formatDateISO(fromValue) : null;
const toDate = toValue ? formatDateISO(toValue) : null;
currentView = 'filter';
currentFilterFrom = fromDate;
currentFilterTo = toDate;
hideMonthNavigation();
loadEntries(fromDate, toDate);
}
/**
* Handle clear filter button click
*/
function handleClearFilter() {
document.getElementById('filterFrom').value = '';
document.getElementById('filterTo').value = '';
currentView = 'monthly';
currentFilterFrom = null;
currentFilterTo = null;
loadMonthlyView();
}
/**
* Handle export button click
*/
async function handleExport(onlyDeviations = false) {
const fromValue = document.getElementById('filterFrom').value;
const toValue = document.getElementById('filterTo').value;
// Validate that both dates are set
if (!fromValue || !toValue) {
showNotification('Bitte beide Datumsfelder ausfüllen', 'error');
return;
}
const fromDate = formatDateISO(fromValue);
const toDate = formatDateISO(toValue);
try {
// Fetch entries for the date range
let entries = await fetchEntries(fromDate, toDate);
// Filter entries if only deviations are requested
if (onlyDeviations) {
entries = entries.filter(entry => Math.abs(entry.netHours - 8.0) > 0.01);
}
// Check if there are any entries
if (entries.length === 0) {
showNotification(
onlyDeviations ?
'Keine Abweichungen vom 8h-Standard im gewählten Zeitraum.' :
'Keine Einträge im gewählten Zeitraum.',
'error'
);
return;
}
// Create CSV content
const csvHeader = 'Datum;Beginn;Ende;Pause (min);Netto (h);Arbeitsort;Abweichung (h)\n';
const csvRows = entries.map(entry => {
const deviation = entry.netHours - 8.0;
const deviationStr = (deviation >= 0 ? '+' : '') + deviation.toFixed(2);
const locationText = entry.location === 'home' ? 'Home' : 'Büro';
return `${entry.date};${entry.startTime};${entry.endTime};${entry.pauseMinutes};${entry.netHours.toFixed(2)};${locationText};${deviationStr}`;
}).join('\n');
const csvContent = csvHeader + csvRows;
// Create download link
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const fileName = onlyDeviations ?
`zeiterfassung_abweichungen_${fromDate}_${toDate}.csv` :
`zeiterfassung_${fromDate}_${toDate}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
const message = onlyDeviations ?
`${entries.length} Abweichung(en) exportiert` :
`${entries.length} Eintrag/Einträge exportiert`;
showNotification(message, 'success');
} catch (error) {
console.error('Error exporting CSV:', error);
showNotification('Fehler beim CSV-Export', 'error');
}
}
/**
* Show a temporary notification
*/
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white z-50 ${
type === 'success' ? 'bg-green-600' :
type === 'error' ? 'bg-red-600' :
'bg-blue-600'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transition = 'opacity 0.5s';
notification.style.opacity = '0';
setTimeout(() => document.body.removeChild(notification), 500);
}, 3000);
}
// ============================================
// FLATPICKR INITIALIZATION
// ============================================
/**
* Initialize all Flatpickr instances with optimized settings
*/
function initializeFlatpickr() {
// German locale
flatpickr.localize(flatpickr.l10ns.de);
// Date pickers - using DD.MM.YYYY format
const dateConfig = {
dateFormat: 'd.m.Y',
locale: 'de',
allowInput: true
};
filterFromPicker = flatpickr('#filterFrom', dateConfig);
filterToPicker = flatpickr('#filterTo', dateConfig);
datePicker = flatpickr('#modalDate', dateConfig);
// Time pickers with enhanced configuration
// For a better "tumbler" experience on desktop, we use:
// - enableTime: true
// - noCalendar: true (time only)
// - time_24hr: true (24-hour format)
// - minuteIncrement: 15 (15-minute steps)
// - Mobile browsers will show native time picker automatically
const timeConfig = {
enableTime: true,
noCalendar: true,
dateFormat: 'H:i',
time_24hr: true,
minuteIncrement: 15,
allowInput: true,
locale: 'de',
// Mobile devices will use native picker which has wheel/tumbler interface
// Desktop will use Flatpickr's time picker with improved UX
static: false,
// When opening the picker, use the current value in the input field
onOpen: function(selectedDates, dateStr, instance) {
const inputValue = instance.input.value;
if (inputValue && inputValue.match(/^\d{1,2}:\d{2}$/)) {
instance.setDate(inputValue, false);
}
}
};
startTimePicker = flatpickr('#modalStartTime', timeConfig);
endTimePicker = flatpickr('#modalEndTime', timeConfig);
// Initialize manual start time picker
manualStartTimePicker = flatpickr('#manualTimeInput', {
enableTime: true,
noCalendar: true,
dateFormat: 'H:i',
time_24hr: true,
minuteIncrement: 15,
allowInput: true,
locale: 'de',
defaultHour: 9,
defaultMinute: 0,
onOpen: function(selectedDates, dateStr, instance) {
const inputValue = instance.input.value;
if (inputValue && inputValue.match(/^\d{1,2}:\d{2}$/)) {
instance.setDate(inputValue, false);
}
}
});
}
/**
* Handle manual start time input
*/
async function handleManualStartTime(timeStr) {
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
// Parse the time
const [hours, minutes] = timeStr.split(':').map(Number);
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, 0);
// Check if entry for today already exists
const entries = await fetchEntries(today, today);
if (entries.length > 0) {
showNotification('Für heute existiert bereits ein Eintrag', 'error');
return;
}
// Create entry with same start and end time (indicates running)
const entry = await createEntry(formatDateDisplay(today), timeStr, timeStr, null);
if (!entry) {
return;
}
currentEntryId = entry.id;
timerStartTime = startDate.getTime();
timerPausedDuration = 0;
isPaused = false;
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
startBtn.disabled = true;
startBtn.classList.add('opacity-50', 'cursor-not-allowed');
startBtn.classList.remove('hover:bg-green-700');
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
stopBtn.classList.add('hover:bg-red-700');
document.getElementById('timerStatus').textContent = 'Läuft seit ' + timeStr;
document.getElementById('timerStatus').classList.remove('cursor-pointer', 'underline', 'hover:text-blue-300');
document.getElementById('timerStatus').classList.add('cursor-default');
// Start timer interval
timerInterval = setInterval(updateTimer, 1000);
updateTimer(); // Immediate update
// Calculate offset for pause scheduling
const timeSinceStart = now.getTime() - startDate.getTime();
const offsetMinutes = Math.floor(timeSinceStart / 60000);
schedulePausesWithOffset(offsetMinutes);
// Reload view to show entry
await loadMonthlyView();
showNotification(`Timer gestartet ab ${timeStr}`, 'success');
}
// ============================================
// EVENT LISTENERS
// ============================================
function initializeEventListeners() {
// Auto-fill month button
document.getElementById('btnAutoFill').addEventListener('click', handleAutoFillMonth);
// Bulk edit toggle
document.getElementById('btnToggleBulkEdit').addEventListener('click', toggleBulkEditMode);
// Master checkbox
document.getElementById('masterCheckbox').addEventListener('change', handleMasterCheckboxToggle);
// Bulk edit actions
document.getElementById('btnSelectAll').addEventListener('click', selectAllEntries);
document.getElementById('btnDeselectAll').addEventListener('click', deselectAllEntries);
document.getElementById('btnBulkSetOffice').addEventListener('click', () => bulkSetLocation('office'));
document.getElementById('btnBulkSetHome').addEventListener('click', () => bulkSetLocation('home'));
document.getElementById('btnBulkSetVacation').addEventListener('click', bulkSetVacation);
document.getElementById('btnBulkSetFlextime').addEventListener('click', bulkSetFlextime);
document.getElementById('btnBulkDelete').addEventListener('click', bulkDeleteEntries);
// Cancel modal button
document.getElementById('btnCancelModal').addEventListener('click', closeModal);
// Location buttons
document.getElementById('btnLocationOffice').addEventListener('click', () => {
document.getElementById('modalLocation').value = 'office';
updateLocationButtons('office');
});
document.getElementById('btnLocationHome').addEventListener('click', () => {
document.getElementById('modalLocation').value = 'home';
updateLocationButtons('home');
});
// Form submission
document.getElementById('entryForm').addEventListener('submit', handleFormSubmit);
// Filter buttons
document.getElementById('btnFilter').addEventListener('click', handleFilter);
document.getElementById('btnClearFilter').addEventListener('click', handleClearFilter);
// Month navigation
document.getElementById('btnPrevMonth').addEventListener('click', handlePrevMonth);
document.getElementById('btnNextMonth').addEventListener('click', handleNextMonth);
// Export buttons
document.getElementById('btnExport').addEventListener('click', () => handleExport(false));
document.getElementById('btnExportDeviations').addEventListener('click', () => handleExport(true));
// Timer buttons
document.getElementById('btnStartWork').addEventListener('click', startWork);
document.getElementById('btnStopWork').addEventListener('click', stopWork);
// Timer status - manual start time entry
document.getElementById('timerStatus').addEventListener('click', () => {
if (!timerStartTime) {
// Show custom time picker modal
document.getElementById('manualTimePickerModal').classList.remove('hidden');
// Set default time to 09:00
manualStartTimePicker.setDate('09:00', false);
}
});
// Manual time picker - Confirm button
document.getElementById('btnConfirmManualTime').addEventListener('click', async () => {
const timeInput = document.getElementById('manualTimeInput');
const timeStr = timeInput.value; // Format: HH:MM from Flatpickr
if (timeStr && timeStr.match(/^\d{1,2}:\d{2}$/)) {
await handleManualStartTime(timeStr);
document.getElementById('manualTimePickerModal').classList.add('hidden');
} else {
showNotification('Bitte wählen Sie eine gültige Zeit aus', 'error');
}
});
// Manual time picker - Cancel button
document.getElementById('btnCancelManualTime').addEventListener('click', () => {
document.getElementById('manualTimePickerModal').classList.add('hidden');
});
// Close manual time picker when clicking outside
document.getElementById('manualTimePickerModal').addEventListener('click', (e) => {
if (e.target.id === 'manualTimePickerModal') {
document.getElementById('manualTimePickerModal').classList.add('hidden');
}
});
// Bundesland selection
document.getElementById('bundeslandSelect').addEventListener('change', handleBundeslandChange);
// Vacation days input
document.getElementById('vacationDaysInput').addEventListener('change', handleVacationDaysChange);
// Close modal when clicking outside
document.getElementById('entryModal').addEventListener('click', (e) => {
if (e.target.id === 'entryModal') {
closeModal();
}
});
// Quick time buttons
document.querySelectorAll('.quick-time-btn').forEach(btn => {
btn.addEventListener('click', () => handleQuickTimeButton(parseInt(btn.dataset.hours)));
});
}
// ============================================
// INITIALIZATION
// ============================================
document.addEventListener('DOMContentLoaded', async () => {
initializeFlatpickr();
initializeEventListeners();
await loadSettings(); // Load saved settings first
checkRunningTimer(); // Check if timer was running
loadMonthlyView(); // Load monthly view by default
});