diff --git a/public/index.html b/public/index.html index 60f0f38..f37e44b 100644 --- a/public/index.html +++ b/public/index.html @@ -856,6 +856,23 @@ + +
+
+ +
+ + +
+
+
+

diff --git a/public/js/holidays.js b/public/js/holidays.js index be0b3fa..7e215fe 100644 --- a/public/js/holidays.js +++ b/public/js/holidays.js @@ -45,6 +45,15 @@ function getPublicHolidays(year, bundesland) { holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' }); holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' }); + // Company-provided holiday: Christmas Eve (24.12) or New Year's Eve (31.12) + // Default to Christmas if companyHolidayPreference is not defined + const companyHolidayPref = typeof companyHolidayPreference !== 'undefined' ? companyHolidayPreference : 'christmas'; + if (companyHolidayPref === 'christmas') { + holidays.push({ date: new Date(year, 11, 24), name: 'Heiligabend (Betriebsfrei)' }); + } else if (companyHolidayPref === 'newyearseve') { + holidays.push({ date: new Date(year, 11, 31), name: 'Silvester (Betriebsfrei)' }); + } + // 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' }); diff --git a/public/js/main.js b/public/js/main.js index 1f77f6e..bc22ebd 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -21,109 +21,6 @@ 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 */ @@ -132,156 +29,6 @@ function getDayOfWeek(date) { 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 */ @@ -641,134 +388,9 @@ function pauseTimer(durationSeconds) { }, 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; - } -} +// ============================================ +// SETTINGS API +// ============================================ /** * Get a setting by key @@ -1142,9 +764,11 @@ function renderMonthlyView(entries) {
+ ${!holidayName ? ` + ` : ''} ${!weekend ? `