diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0392ea3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## v0.40.0 + +- Bundesland-Auswahl für Feiertage ergänzt +- Unterstützte Bundesländer: Bayern, Niedersachsen, Hamburg, Nordrhein-Westfalen +- Zusatzoption: Bayern inkl. Mariä Himmelfahrt +- Feiertage werden automatisch aus dem Datum berechnet +- Feiertage werden mit 0:00 Sollzeit bewertet +- Arbeit an Feiertagen zählt vollständig als Pluszeit +- Feiertagsspalte in Tagesdetails und CSV-Export ergänzt + +## v0.30.0 + +- Sollzeit Montag bis Freitag = 8:00 Stunden +- Sollzeit Samstag und Sonntag = 0:00 Stunden +- Wochenendarbeit zählt vollständig als Pluszeit diff --git a/README.md b/README.md index 79f4c4f..fc54f13 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,20 @@ -\# qplanner-arbeitszeit-auswertung - - +# qplanner-arbeitszeit-auswertung Browserbasierte Auswertung von qPlanner CSV-Exporten zur Berechnung von Plus- und Minusstunden. +## Funktionen +- CSV-Upload im Browser +- Auswertung der Netto-Arbeitszeit +- Sollzeit Montag bis Freitag: 8:00 Stunden +- Sollzeit Samstag und Sonntag: 0:00 Stunden +- Gesetzliche Feiertage je Bundesland: 0:00 Stunden +- Unterstützte Bundesländer: Bayern, Niedersachsen, Hamburg, Nordrhein-Westfalen +- Zusatzoption: Bayern inkl. Mariä Himmelfahrt +- Monatsübersicht +- Tagesdetails inklusive Wochentag und Feiertag +- CSV-Export -\## Funktionen - - - -\- CSV-Upload im Browser - -\- Auswertung der Netto-Arbeitszeit - -\- Sollzeit Montag bis Freitag: 8:00 Stunden - -\- Sollzeit Samstag und Sonntag: 0:00 Stunden - -\- Monatsübersicht - -\- Tagesdetails - -\- CSV-Export - - - -\## Datenschutz - - - -Die CSV-Datei wird ausschließlich lokal im Browser verarbeitet. +## Datenschutz +Die CSV-Datei wird ausschließlich lokal im Browser verarbeitet. Es findet kein Upload auf einen Server statt. diff --git a/app.js b/app.js index 2e8497c..312c812 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ const fileInput = document.getElementById('csvFile'); const dropZone = document.getElementById('dropZone'); const targetTimeInput = document.getElementById('targetTime'); +const federalStateSelect = document.getElementById('federalState'); const errorBox = document.getElementById('errorBox'); const summary = document.getElementById('summary'); const results = document.getElementById('results'); @@ -21,6 +22,10 @@ targetTimeInput.addEventListener('change', () => { if (lastFileText) analyseCsv(lastFileText); }); +federalStateSelect.addEventListener('change', () => { + if (lastFileText) analyseCsv(lastFileText); +}); + exportButton.addEventListener('click', () => { if (lastResult) exportResultCsv(lastResult); }); @@ -70,15 +75,16 @@ function analyseCsv(text) { const dataRows = rows.slice(1).filter(row => row.some(cell => String(cell).trim() !== '')); const targetMinutes = timeToMinutes(targetTimeInput.value || '08:00'); + const federalState = federalStateSelect.value || 'NW'; const detectedFormat = detectFormat(headers); let days; if (detectedFormat === 'daily') { - days = analyseDailyFormat(headers, dataRows, targetMinutes); - formatHint.textContent = 'Format erkannt: Tagesauswertung. Montag bis Freitag haben Sollzeit, Samstag und Sonntag werden mit 0:00 Sollzeit berechnet. Zeilen ohne Arbeitszeit werden ignoriert.'; + days = analyseDailyFormat(headers, dataRows, targetMinutes, federalState); + formatHint.textContent = `Format erkannt: Tagesauswertung. Montag bis Freitag haben Sollzeit, Samstag/Sonntag und Feiertage in ${stateLabel(federalState)} werden mit 0:00 Sollzeit berechnet. Zeilen ohne Arbeitszeit werden ignoriert.`; } else if (detectedFormat === 'monthly') { - days = analyseMonthlyFormat(headers, dataRows, targetMinutes); - formatHint.textContent = 'Format erkannt: Monatsauswertung. Arbeitszeiten werden aus dem Monatsblock gelesen.'; + days = analyseMonthlyFormat(headers, dataRows, targetMinutes, federalState); + formatHint.textContent = `Format erkannt: Monatsauswertung. Arbeitszeiten werden aus dem Monatsblock gelesen. Wenn ein Datum erkannt wird, werden Wochenenden und Feiertage in ${stateLabel(federalState)} mit 0:00 Sollzeit berechnet.`; } else { throw new Error('Das CSV-Format wurde nicht erkannt. Benötigt wird entweder die Spalte "Tag" oder "Monat" plus "Dauer Arbeitstag".'); } @@ -93,7 +99,7 @@ function analyseCsv(text) { saldoMinutes: days.reduce((sum, day) => sum + day.saldoMinutes, 0) }; - lastResult = { months, days, totals }; + lastResult = { months, days, totals, federalState, federalStateLabel: stateLabel(federalState) }; render(lastResult); } catch (error) { showError(error.message); @@ -108,7 +114,7 @@ function detectFormat(headers) { return 'unknown'; } -function analyseDailyFormat(headers, dataRows, targetMinutes) { +function analyseDailyFormat(headers, dataRows, targetMinutes, federalState) { const dateIndex = headers.indexOf('Tag'); const startIndex = headers.indexOf('Arbeitstag Beginn'); const endIndex = headers.indexOf('Arbeitstag Ende'); @@ -132,8 +138,9 @@ function analyseDailyFormat(headers, dataRows, targetMinutes) { end: cleanCell(row[endIndex]), workMinutes, weekday: weekdayNameFromDate(date), - targetMinutes: targetMinutesForDate(date, targetMinutes), - saldoMinutes: workMinutes - targetMinutesForDate(date, targetMinutes), + holiday: holidayNameForDate(date, federalState), + targetMinutes: targetMinutesForDate(date, targetMinutes, federalState), + saldoMinutes: workMinutes - targetMinutesForDate(date, targetMinutes, federalState), breakOk: cleanCell(row[breakIndex]), warnings: cleanCell(row[warningIndex]) }); @@ -141,7 +148,7 @@ function analyseDailyFormat(headers, dataRows, targetMinutes) { return days; } -function analyseMonthlyFormat(headers, dataRows, targetMinutes) { +function analyseMonthlyFormat(headers, dataRows, targetMinutes, federalState) { const monthIndex = headers.indexOf('Monat'); const durationIndex = headers.indexOf('Dauer Arbeitstag'); const stampIndex = headers.indexOf('Stempelzeit'); @@ -161,8 +168,9 @@ function analyseMonthlyFormat(headers, dataRows, targetMinutes) { end: '', workMinutes, weekday: weekdayNameFromDate(date), - targetMinutes: targetMinutesForDate(date, targetMinutes), - saldoMinutes: workMinutes - targetMinutesForDate(date, targetMinutes), + holiday: holidayNameForDate(date, federalState), + targetMinutes: targetMinutesForDate(date, targetMinutes, federalState), + saldoMinutes: workMinutes - targetMinutesForDate(date, targetMinutes, federalState), breakOk: '', warnings: '' }); @@ -172,13 +180,111 @@ function analyseMonthlyFormat(headers, dataRows, targetMinutes) { } -function targetMinutesForDate(date, weekdayTargetMinutes) { +function targetMinutesForDate(date, weekdayTargetMinutes, federalState) { const weekday = getWeekdayIndex(date); // 0 = Sonntag, 6 = Samstag. Rufbereitschafts-/Wochenendarbeit zählt komplett als Pluszeit. if (weekday === 0 || weekday === 6) return 0; + if (holidayNameForDate(date, federalState)) return 0; return weekdayTargetMinutes; } + +function stateLabel(state) { + const labels = { + NW: 'Nordrhein-Westfalen', + BY: 'Bayern', + BY_MH: 'Bayern inkl. Mariä Himmelfahrt', + NI: 'Niedersachsen', + HH: 'Hamburg' + }; + return labels[state] || 'Nordrhein-Westfalen'; +} + +function parseGermanDate(date) { + const match = String(date).match(/^(\d{2})\.(\d{2})\.(\d{4})$/); + if (!match) return null; + return { + day: Number(match[1]), + month: Number(match[2]), + year: Number(match[3]), + key: `${match[3]}-${match[2]}-${match[1]}` + }; +} + +function dateKeyFromDate(year, month, day) { + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; +} + +function addDaysKey(date, offset) { + const copy = new Date(date.getFullYear(), date.getMonth(), date.getDate() + offset); + return dateKeyFromDate(copy.getFullYear(), copy.getMonth() + 1, copy.getDate()); +} + +function easterSunday(year) { + // Gaußsche Osterformel für den gregorianischen Kalender. + 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); +} + +function holidaysForYear(year, federalState) { + const easter = easterSunday(year); + const holidays = new Map(); + + const add = (month, day, name) => holidays.set(dateKeyFromDate(year, month, day), name); + const addRelative = (offset, name) => holidays.set(addDaysKey(easter, offset), name); + + // Bundesweite Feiertage + add(1, 1, 'Neujahr'); + addRelative(-2, 'Karfreitag'); + addRelative(1, 'Ostermontag'); + add(5, 1, 'Tag der Arbeit'); + addRelative(39, 'Christi Himmelfahrt'); + addRelative(50, 'Pfingstmontag'); + add(10, 3, 'Tag der Deutschen Einheit'); + add(12, 25, '1. Weihnachtstag'); + add(12, 26, '2. Weihnachtstag'); + + if (federalState === 'BY' || federalState === 'BY_MH') { + add(1, 6, 'Heilige Drei Könige'); + addRelative(60, 'Fronleichnam'); + add(11, 1, 'Allerheiligen'); + } + + if (federalState === 'BY_MH') { + add(8, 15, 'Mariä Himmelfahrt'); + } + + if (federalState === 'NW') { + addRelative(60, 'Fronleichnam'); + add(11, 1, 'Allerheiligen'); + } + + if (federalState === 'NI' || federalState === 'HH') { + add(10, 31, 'Reformationstag'); + } + + return holidays; +} + +function holidayNameForDate(date, federalState) { + const parsed = parseGermanDate(date); + if (!parsed) return ''; + return holidaysForYear(parsed.year, federalState).get(parsed.key) || ''; +} + function getWeekdayIndex(date) { const match = String(date).match(/^(\d{2})\.(\d{2})\.(\d{4})$/); if (!match) return null; @@ -330,6 +436,7 @@ function render(result) {
CSV-Auswertung
CSV-Datei hochladen, Netto-Arbeitszeiten auslesen und Plus-/Minusstunden berechnen: Montag bis Freitag mit 8:00 Stunden Sollzeit, Samstag/Sonntag mit 0:00 Sollzeit.
+CSV-Datei hochladen, Netto-Arbeitszeiten auslesen und Plus-/Minusstunden berechnen: Montag bis Freitag mit 8:00 Stunden Sollzeit, Samstag/Sonntag und gesetzliche Feiertage mit 0:00 Sollzeit.