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).
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;