From 3cdcf7a541c91061b6ffcb2a3fdebb33a579b667 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:04:56 +0000 Subject: [PATCH 1/2] Initial plan From 5c9ab77ffd77b0880b23e6dfd09cd1e96910f828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:23:39 +0000 Subject: [PATCH 2/2] Add Excel/CSV export feature for beginner-friendly data export - Add CSV export button to Dienstplan_Portable.html - Add CSV export button to webapp (index.html, app.js, styles.css) - Export includes all duties and monthly summary with calculations - Use semicolon separator for German Excel compatibility - Add UTF-8 BOM for proper character encoding - Add proper CSV escaping for names with special characters - Add helpful tip explaining CSV compatibility with Excel/LibreOffice/Google Sheets Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- Dienstplan_Portable.html | 121 +++++++++++++++++++++++++++++++++++++++ webapp/app.js | 107 ++++++++++++++++++++++++++++++++++ webapp/index.html | 2 + webapp/styles.css | 12 ++++ 4 files changed, 242 insertions(+) diff --git a/Dienstplan_Portable.html b/Dienstplan_Portable.html index 77418ed..50cedd3 100644 --- a/Dienstplan_Portable.html +++ b/Dienstplan_Portable.html @@ -460,10 +460,12 @@

Daten werden automatisch im Browser gespeichert (localStorage).

+
+

💡 Tipp: CSV-Dateien können direkt mit Excel, LibreOffice oder Google Sheets geöffnet werden.

Gefahrenzone

@@ -963,6 +965,125 @@ class DienstplanApp { downloadAnchor.remove(); } + /** + * Export data as CSV (Excel-compatible) - Beginner-friendly format + * Exports two sheets: 1) All duties (Dienste) 2) Monthly summary (Auswertung) + */ + exportCSV() { + const m = parseInt(document.getElementById('selectMonth').value); + const y = parseInt(document.getElementById('selectYear').value); + + // Helper function to escape CSV values (handles semicolons, quotes, newlines) + const escapeCSV = (value) => { + const str = String(value); + if (str.includes(';') || str.includes('"') || str.includes('\n')) { + return '"' + str.replace(/"/g, '""') + '"'; + } + return str; + }; + + // Build CSV content with BOM for Excel UTF-8 support + let csv = '\uFEFF'; // UTF-8 BOM for Excel + + // === Sheet 1: Dienste (All Duties) === + csv += 'ALLE DIENSTE\n'; + csv += 'Datum;Wochentag;Mitarbeiter;Anteil;Tagestyp\n'; + + // Sort duties by date + const sortedDuties = [...this.duties].sort((a, b) => a.date.localeCompare(b.date)); + + sortedDuties.forEach(duty => { + const date = new Date(duty.date); + const dayInfo = this.getDayTypeInfo(date); + const isQual = this.isQualifyingDay(date); + const dateStr = date.toLocaleDateString('de-DE'); + const weekday = WEEKDAYS[date.getDay()]; + const dayType = isQual ? 'WE-Tag' : 'Werktag (WT)'; + + csv += `${dateStr};${weekday};${escapeCSV(duty.name)};${duty.share.toFixed(1).replace('.', ',')};${dayType}\n`; + }); + + csv += '\n\n'; + + // === Sheet 2: Monatliche Auswertung === + csv += `AUSWERTUNG ${MONTHS[m]} ${y}\n`; + csv += 'Mitarbeiter;WT (Einheiten);WE Freitag;WE Andere;WE Gesamt;Schwelle erreicht;Auszahlung (EUR)\n'; + + // Filter duties for selected month + const monthDuties = this.duties.filter(d => { + const date = new Date(d.date); + return date.getMonth() === m && date.getFullYear() === y; + }); + + // Group by employee + const stats = {}; + monthDuties.forEach(d => { + if (!stats[d.name]) { + stats[d.name] = { wt: 0, we_fr: 0, we_other: 0 }; + } + + const date = new Date(d.date); + const isQual = this.isQualifyingDay(date); + const isFri = this.isFriday(date); + + if (!isQual) { + stats[d.name].wt += d.share; + } else { + if (isFri) { + stats[d.name].we_fr += d.share; + } else { + stats[d.name].we_other += d.share; + } + } + }); + + let totalPayout = 0; + + for (const [name, data] of Object.entries(stats)) { + const we_total = data.we_fr + data.we_other; + const thresholdReached = we_total >= (CONFIG.THRESHOLD - CONFIG.TOLERANCE); + + let payout = 0; + + if (thresholdReached) { + const wt_pay = data.wt * CONFIG.RATE_WT; + let deduct = CONFIG.DEDUCTION; + const deduct_fr = Math.min(deduct, data.we_fr); + const deduct_other = Math.max(0, deduct - deduct_fr); + const paid_fr = Math.max(0, data.we_fr - deduct_fr); + const paid_other = Math.max(0, data.we_other - deduct_other); + const we_pay = (paid_fr + paid_other) * CONFIG.RATE_WE; + payout = wt_pay + we_pay; + } + + totalPayout += payout; + + const threshold = thresholdReached ? 'JA' : 'NEIN'; + + csv += `${escapeCSV(name)};${data.wt.toFixed(1).replace('.', ',')};${data.we_fr.toFixed(1).replace('.', ',')};${data.we_other.toFixed(1).replace('.', ',')};${we_total.toFixed(1).replace('.', ',')};${threshold};${payout.toFixed(2).replace('.', ',')}\n`; + } + + csv += `\nGESAMT;;;;;;${totalPayout.toFixed(2).replace('.', ',')}\n`; + + csv += '\n\n'; + csv += 'LEGENDE\n'; + csv += 'WT;Werktag (Montag-Donnerstag ohne Feiertag/Vortag)\n'; + csv += 'WE-Tag;"Freitag, Samstag, Sonntag, Feiertag oder Tag vor Feiertag"\n'; + csv += 'Schwelle;"Mindestens 2,0 WE-Einheiten für Bonuszahlung erforderlich"\n'; + csv += 'Sätze;"WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 1,0 Abzug)"\n'; + + // Download CSV file + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const downloadAnchor = document.createElement('a'); + downloadAnchor.href = url; + downloadAnchor.download = `Dienstplan_${y}_${String(m + 1).padStart(2, '0')}.csv`; + document.body.appendChild(downloadAnchor); + downloadAnchor.click(); + downloadAnchor.remove(); + URL.revokeObjectURL(url); + } + importData(input) { const file = input.files[0]; if (!file) return; diff --git a/webapp/app.js b/webapp/app.js index 41af5ee..ba13f91 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -50,6 +50,7 @@ class DienstplanApp { document.getElementById('calculate-btn').addEventListener('click', () => this.calculateBonuses()); // Settings + document.getElementById('export-csv-btn').addEventListener('click', () => this.exportCSV()); document.getElementById('export-btn').addEventListener('click', () => this.exportData()); document.getElementById('import-btn').addEventListener('click', () => this.importData()); document.getElementById('clear-all-btn').addEventListener('click', () => this.clearAllData()); @@ -434,6 +435,112 @@ class DienstplanApp { this.showToast('Daten wurden exportiert.', 'success'); } + /** + * Export data as CSV (Excel-compatible) - Beginner-friendly format + * Exports all duties and monthly summary for the selected month + */ + exportCSV() { + const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; + const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; + + const month = parseInt(document.getElementById('calc-month-select').value); + const year = parseInt(document.getElementById('calc-year-select').value); + + // Helper function to escape CSV values (handles semicolons, quotes, newlines) + const escapeCSV = (value) => { + const str = String(value); + if (str.includes(';') || str.includes('"') || str.includes('\n')) { + return '"' + str.replace(/"/g, '""') + '"'; + } + return str; + }; + + // Build CSV content with BOM for Excel UTF-8 support + let csv = '\uFEFF'; // UTF-8 BOM for Excel + + // === Sheet 1: Dienste (All Duties for the month) === + csv += `DIENSTE ${monthNames[month - 1]} ${year}\n`; + csv += 'Datum;Wochentag;Mitarbeiter;Anteil;Tagestyp\n'; + + const employees = this.storage.getEmployees(); + const allDuties = []; + + // Collect all duties for the selected month from all employees + employees.forEach(employee => { + const duties = this.storage.getDutiesForMonth(employee, year, month); + duties.forEach(duty => { + allDuties.push({ + ...duty, + employee: employee + }); + }); + }); + + // Sort by date + allDuties.sort((a, b) => a.date - b.date); + + allDuties.forEach(duty => { + const isQual = this.calculator.isQualifyingDay(duty.date); + const dateStr = duty.date.toLocaleDateString('de-DE'); + const weekday = weekdays[duty.date.getDay()]; + const dayType = isQual ? 'WE-Tag' : 'Werktag (WT)'; + + csv += `${dateStr};${weekday};${escapeCSV(duty.employee)};${duty.share.toFixed(1).replace('.', ',')};${dayType}\n`; + }); + + csv += '\n\n'; + + // === Sheet 2: Monatliche Auswertung === + csv += `AUSWERTUNG ${monthNames[month - 1]} ${year}\n`; + csv += 'Mitarbeiter;Normale Tage;WE/Feiertag Tage;Abzug;Normale Tage (bezahlt);WE/Feiertag (bezahlt);Schwelle erreicht;Bonus Normal;Bonus WE;Gesamtbonus (EUR)\n'; + + const employeeDuties = this.storage.getAllEmployeeDutiesForMonth(year, month); + const results = this.calculator.calculateAllEmployees(employeeDuties); + + let totalBonus = 0; + + for (const [employeeName, result] of Object.entries(results)) { + const threshold = result.thresholdReached ? 'JA' : 'NEIN'; + + totalBonus += result.totalBonus; + + csv += `${escapeCSV(employeeName)};`; + csv += `${result.normalDays.toFixed(1).replace('.', ',')};`; + csv += `${result.qualifyingDays.toFixed(1).replace('.', ',')};`; + csv += `${result.qualifyingDaysDeducted.toFixed(1).replace('.', ',')};`; + csv += `${result.normalDaysPaid.toFixed(1).replace('.', ',')};`; + csv += `${result.qualifyingDaysPaid.toFixed(1).replace('.', ',')};`; + csv += `${threshold};`; + csv += `${result.bonusNormalDays.toFixed(2).replace('.', ',')};`; + csv += `${result.bonusQualifyingDays.toFixed(2).replace('.', ',')};`; + csv += `${result.totalBonus.toFixed(2).replace('.', ',')}\n`; + } + + csv += `\nGESAMT;;;;;;;;;${totalBonus.toFixed(2).replace('.', ',')}\n`; + + csv += '\n\n'; + csv += 'LEGENDE\n'; + csv += 'Normale Tage;Montag-Donnerstag ohne Feiertag/Vortag\n'; + csv += 'WE/Feiertag Tage;"Freitag, Samstag, Sonntag, Feiertag oder Tag vor Feiertag"\n'; + csv += 'Schwelle;"Mindestens 2,0 WE-Einheiten für Bonuszahlung erforderlich"\n'; + csv += 'Sätze;"Normale Tage = 250 EUR/Einheit, WE/Feiertag = 450 EUR/Einheit"\n'; + csv += 'Abzug;"Bei Erreichen der Schwelle wird 1,0 WE-Einheit abgezogen"\n'; + + // Download CSV file + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `Dienstplan_${year}_${String(month).padStart(2, '0')}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + this.showToast('CSV wurde exportiert. Öffnen Sie die Datei mit Excel oder LibreOffice.', 'success'); + } + /** * Import data from JSON file */ diff --git a/webapp/index.html b/webapp/index.html index 9cbafd6..ce8f0da 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -177,7 +177,9 @@

Datenexport / Import

+ +

💡 Tipp: CSV-Dateien können direkt mit Excel, LibreOffice oder Google Sheets geöffnet werden.

diff --git a/webapp/styles.css b/webapp/styles.css index f332984..86c29b8 100644 --- a/webapp/styles.css +++ b/webapp/styles.css @@ -188,6 +188,18 @@ header h1 { background: #5a6268; } +.btn-success { + background: #28a745; + color: white; + margin-right: 10px; +} + +.btn-success:hover { + background: #218838; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4); +} + .btn-danger { background: #dc3545; color: white;