From 89862e918cfb277bb7b2877be4e809974e2bba23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:57:27 +0000 Subject: [PATCH 1/7] Initial plan From a2cc8340ee5bed530ba2376daee9e9d12080b7e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:04:38 +0000 Subject: [PATCH 2/7] Fix deduction value from 2.0 to 1.0 across all files for consistency Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- CHANGELOG.md | 2 +- README.md | 2 +- SPECIFICATION.md | 6 ++-- android-app/README.md | 2 +- .../dienstplan/nrw/data/PayrollCalculator.kt | 8 ++--- .../dienstplan/nrw/PayrollCalculatorTest.kt | 23 ++++++------- src/build_template.py | 4 +-- src/calculate.py | 2 +- webapp/README.md | 8 ++--- webapp/TEST_GUIDE.md | 8 ++--- webapp/calculator.js | 4 +-- webapp/test-suite.js | 32 +++++++++---------- 12 files changed, 51 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5283e..bb01259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Added native Android mobile app for duty roster management with the same NRW Var - Same WT-Tag classification - Same compensation rates (WT: 250€, WE: 450€) - Same threshold logic (≥ 2.0 WE units) -- Same deduction rules (2.0 units, Friday priority) +- Same deduction rules (1.0 unit, Friday priority) - Same Variante 2 behavior (no WE compensation below threshold) **Testing:** diff --git a/README.md b/README.md index 69e94b4..1b333c7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Native Android-App für mobiles Dienstplan-Management. Siehe [android-app/README - ✅ Automatische Erkennung von Wochenenden (Fr–So), Feiertagen und Vortagen - ✅ Vergütungslogik: WT 250€, WE 450€ (nur ab Schwelle ≥ 2,0 WE-Einheiten) -- ✅ Abzug 2,0 WE-Einheiten (Freitag-Priorität) nach Erreichen der Schwelle +- ✅ Abzug 1,0 WE-Einheit (Freitag-Priorität) nach Erreichen der Schwelle - ✅ Vorbefüllte Monatsvorlagen mit allen Datumswerten - ✅ Excel-kompatibel (ohne Office 365 Funktionen) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 6021df3..213d6a5 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -4,7 +4,7 @@ Stand: 14.11.2025 (Deutschland) ## Ziel -Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt – inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 1,0 WE-Einheit. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden stets vergütet. +Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt – inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 1,0 WE-Einheit nach Erreichen der Schwelle. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden ebenfalls nur bei Erreichen der WE-Schwelle vergütet. Hinweise: - Region: Deutschland, Bundesland wählbar (steuert Feiertage). @@ -30,7 +30,7 @@ Hinweise: - **WE** (WE-Tag): - Wenn Monats-Summe WE-Einheiten < 2,0 → Auszahlung 0 € für alle WE-Einheiten. - Wenn Monats-Summe WE-Einheiten ≥ 2,0 → Auszahlung 450 €/WE-Einheit, - anschließend Abzug genau 2,0 WE-Einheiten (max. 1× pro Person/Monat). + anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat). - Abzugs-Priorität: zuerst aus Freitag-WE-Einheiten, Rest aus den übrigen WE-Einheiten (Sa/So/Feiertag/Vortag). Chronologie muss nicht nachgebildet werden; es genügt die Priorität nach Kategorie. ### Splits/Anteile @@ -51,7 +51,7 @@ Hinweise: - Satz_WT = 250 - Satz_WE = 450 - WE_Schwelle = 2,0 -- Abzug_nach_WE_Schwelle = 2,0 +- Abzug_nach_WE_Schwelle = 1,0 - BL_Auswahl = Dropdown (z. B. BW, BY, BE, …) - Monat_Auswahl = Datum (erster Tag des Zielmonats, z. B. 01.11.2025) - Variante = 2 (fix auf „streng") diff --git a/android-app/README.md b/android-app/README.md index 062be7d..7326ceb 100644 --- a/android-app/README.md +++ b/android-app/README.md @@ -123,7 +123,7 @@ Edit `PayrollCalculator.kt` and modify the constants: - `RATE_WT`: Weekday rate (default 250€) - `RATE_WE`: Weekend rate (default 450€) - `WE_THRESHOLD`: Threshold for WE compensation (default 2.0) -- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 2.0) +- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 1.0) ### Adding Holidays diff --git a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt index 5d8f932..4269a88 100644 --- a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt +++ b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt @@ -12,11 +12,11 @@ import kotlin.math.min * Business rules: * - WE-Tag (Weekend/Holiday): Friday, Saturday, Sunday, public holiday, day before public holiday * - WT-Tag (Weekday): All other days - * - WT compensation: Always 250€ per unit + * - WT compensation: 250€ per unit (only if threshold reached) * - WE compensation: Only paid if monthly total >= 2.0 WE units (threshold) - * - If threshold reached: 450€ per WE unit, then deduct exactly 2.0 WE units + * - If threshold reached: 450€ per WE unit, then deduct exactly 1.0 WE unit * - Deduction priority: Friday first, then other WE days - * - Below threshold: 0€ for WE shifts (NOT converted to WT) + * - Below threshold: 0€ for all shifts (neither WT nor WE) */ class PayrollCalculator { @@ -24,7 +24,7 @@ class PayrollCalculator { private const val RATE_WT = 250.0 // Satz_WT private const val RATE_WE = 450.0 // Satz_WE private const val WE_THRESHOLD = 2.0 // WE_Schwelle - private const val DEDUCTION_AFTER_THRESHOLD = 2.0 // Abzug_nach_WE_Schwelle + private const val DEDUCTION_AFTER_THRESHOLD = 1.0 // Abzug_nach_WE_Schwelle private const val TOLERANCE = 0.0001 // For floating-point comparisons } diff --git a/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt b/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt index dbabf20..0cbef49 100644 --- a/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt +++ b/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt @@ -52,7 +52,7 @@ class PayrollCalculatorTest { /** * Test Case 2: Exactly at threshold (2.0 WE) - * Expected: WE payout = 0€ (0.0 units after deduction), threshold reached + * Expected: WE payout = 450€ (1.0 unit after deduction), threshold reached */ @Test fun testExactlyAtThreshold() { @@ -67,14 +67,14 @@ class PayrollCalculatorTest { val result = results[0] assertEquals(2.0, result.weTotal, 0.001) assertTrue(result.thresholdReached) - assertEquals(2.0, result.deductionTotal, 0.001) - assertEquals(0.0, result.wePaid, 0.001) - assertEquals(0.0, result.payoutWE, 0.001) + assertEquals(1.0, result.deductionTotal, 0.001) + assertEquals(1.0, result.wePaid, 0.001) + assertEquals(450.0, result.payoutWE, 0.001) } /** * Test Case 3: Over threshold (3.5 WE) - * Expected: WE payout = 675€ (1.5 units after deduction) + * Expected: WE payout = 1125€ (2.5 units after 1.0 deduction) */ @Test fun testOverThreshold() { @@ -91,8 +91,8 @@ class PayrollCalculatorTest { val result = results[0] assertEquals(3.5, result.weTotal, 0.001) assertTrue(result.thresholdReached) - assertEquals(1.5, result.wePaid, 0.001) - assertEquals(675.0, result.payoutWE, 0.001) + assertEquals(2.5, result.wePaid, 0.001) + assertEquals(1125.0, result.payoutWE, 0.001) } /** @@ -115,8 +115,9 @@ class PayrollCalculatorTest { assertEquals(0.4, result.weFriday, 0.001) assertEquals(1.6, result.weOther, 0.001) assertEquals(0.4, result.deductionFriday, 0.001) // All Friday deducted first - assertEquals(1.6, result.deductionOther, 0.001) // Rest from other (1.6 to reach 2.0 total) - assertEquals(0.0, result.wePaid, 0.001) + assertEquals(0.6, result.deductionOther, 0.001) // Rest from other (0.6 to reach 1.0 total) + assertEquals(1.0, result.wePaid, 0.001) + assertEquals(450.0, result.payoutWE, 0.001) } /** @@ -147,8 +148,8 @@ class PayrollCalculatorTest { // B: above threshold assertTrue(resultB.thresholdReached) assertEquals(2.5, resultB.weTotal, 0.001) - assertEquals(0.5, resultB.wePaid, 0.001) - assertEquals(225.0, resultB.payoutWE, 0.001) + assertEquals(1.5, resultB.wePaid, 0.001) + assertEquals(675.0, resultB.payoutWE, 0.001) } private fun parseDate(dateString: String): Date { diff --git a/src/build_template.py b/src/build_template.py index 7a6d1b5..1e32aff 100644 --- a/src/build_template.py +++ b/src/build_template.py @@ -61,7 +61,7 @@ def _populate_readme(ws): rules = [ "WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig).", "Variante 2 (streng): WE werden nur vergütet, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden;", - "dann 450 €/WE und Abzug 2,0 (Freitag zuerst). WT werden immer mit 250 € vergütet.", + "dann 450 €/WE und Abzug 1,0 (Freitag zuerst). WT werden bei Erreichen der WE-Schwelle mit 250 € vergütet.", "Splits anteilig. Monat und Bundesland in 'Regeln' wählen.", "", "Schritte:", @@ -83,7 +83,7 @@ def _populate_rules(ws): ("Satz_WT", 250, "Euro für jeden Werktagsdienst (Mo–Do, sofern kein WE-Tag)"), ("Satz_WE", 450, "Euro für jeden WE-Tag (Fr–So, Feiertag, Vortag Feiertag)"), ("WE_Schwelle", 2.0, "Ab dieser WE-Anzahl wird vergütet (sonst 0 €)"), - ("Abzug_nach_WE_Schwelle", 2.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"), + ("Abzug_nach_WE_Schwelle", 1.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"), ("BL_Auswahl", "NRW", "Bundesland (steuert Feiertage)"), ("Monat_Auswahl", date(2025, 11, 1), "Erster Tag des Zielmonats"), ("Variante", 2, "Fix: 2 = streng (WE nur bei Schwelle ≥ 2,0)"), diff --git a/src/calculate.py b/src/calculate.py index f2464e7..1faa1f5 100644 --- a/src/calculate.py +++ b/src/calculate.py @@ -14,7 +14,7 @@ from collections import defaultdict SATZ_WT = 250 # Euro für Werktag SATZ_WE = 450 # Euro für Wochenende WE_SCHWELLE = 2.0 # Mindestanzahl WE-Dienste für Vergütung -ABZUG = 2.0 # Abzug nach Erreichen der Schwelle +ABZUG = 1.0 # Abzug nach Erreichen der Schwelle def load_holidays(wb): diff --git a/webapp/README.md b/webapp/README.md index 12e74a0..bd6db7e 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -21,7 +21,7 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta ### Bonusberechnung 1. **Schwellenwert**: Mindestens **2.0 qualifizierende Tage** im Monat erforderlich -2. **Abzug**: Bei Erreichen des Schwellenwerts werden **2.0 qualifizierende Tage** abgezogen +2. **Abzug**: Bei Erreichen des Schwellenwerts wird **1.0 qualifizierender Tag** abgezogen (Freitag-Priorität) 3. **Vergütung**: - Normale Tage: **250€** pro Tag - Qualifizierende Tage (WE/Feiertag): **450€** pro Tag @@ -34,9 +34,9 @@ Mitarbeiter hat im Monat: **Berechnung**: - Qualifizierende Tage: 3.0 (Schwellenwert erreicht ✓) -- Abzug: -2.0 qualifizierende Tage -- Bezahlt: 3 normale Tage + 1 qualifizierender Tag -- **Bonus**: (3 × 250€) + (1 × 450€) = **1.200€** +- Abzug: -1.0 qualifizierender Tag +- Bezahlt: 3 normale Tage + 2 qualifizierende Tage +- **Bonus**: (3 × 250€) + (2 × 450€) = **1.650€** ## Installation & Nutzung diff --git a/webapp/TEST_GUIDE.md b/webapp/TEST_GUIDE.md index b8e29f0..7722ab8 100644 --- a/webapp/TEST_GUIDE.md +++ b/webapp/TEST_GUIDE.md @@ -84,8 +84,8 @@ Dienste: Erwartung: - Qualifizierende Tage: 2.0 - Schwellenwert: ✅ Erreicht -- Abzug: -2.0 -- Bezahlt: 0.0 × 450€ = 0€ +- Abzug: -1.0 +- Bezahlt: 1.0 × 450€ = 450€ ``` ### Beispiel 2: Gemischte Dienste @@ -96,8 +96,8 @@ Dienste: Erwartung: - Normale Tage: 2.0 × 250€ = 500€ -- Qualifizierende Tage: (2.0 - 2.0) × 450€ = 0€ -- Gesamt: 500€ +- Qualifizierende Tage: (2.0 - 1.0) × 450€ = 450€ +- Gesamt: 950€ ``` ### Beispiel 3: Halbe Dienste diff --git a/webapp/calculator.js b/webapp/calculator.js index d705419..bc5c787 100644 --- a/webapp/calculator.js +++ b/webapp/calculator.js @@ -108,8 +108,8 @@ class BonusCalculator { let totalDeduction = 0; if (thresholdReached) { - // Deduct 2.0 qualifying days with Friday priority - totalDeduction = 2.0; + // Deduct 1.0 qualifying day with Friday priority + totalDeduction = 1.0; // First deduct from Friday deductionFromFriday = Math.min(totalDeduction, qualifyingDaysFriday); diff --git a/webapp/test-suite.js b/webapp/test-suite.js index 03e85a9..9b7272b 100644 --- a/webapp/test-suite.js +++ b/webapp/test-suite.js @@ -204,12 +204,12 @@ runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => { t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.qualifyingDaysDeducted, 2.0, 'Sollte 2.0 Tage abziehen'); - t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen'); - t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); + t.assertEqual(result.qualifyingDaysDeducted, 1.0, 'Sollte 1.0 Tag abziehen'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); }); -runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, aber nach Abzug nichts)', (t) => { +runner.test('Berechnung: 2x halbe WE-Dienste = 450€ (genau Schwelle, nach Abzug 1.0)', (t) => { const holidays = new HolidayProvider(); const calculator = new BonusCalculator(holidays); @@ -224,11 +224,11 @@ runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, aber nach A t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben (4×0.5)'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen nach Abzug'); - t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen nach Abzug'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); }); -runner.test('Berechnung: 3 WE-Tage = 450€', (t) => { +runner.test('Berechnung: 3 WE-Tage = 900€', (t) => { const holidays = new HolidayProvider(); const calculator = new BonusCalculator(holidays); @@ -241,8 +241,8 @@ runner.test('Berechnung: 3 WE-Tage = 450€', (t) => { const result = calculator.calculateMonthlyBonus(duties); t.assertEqual(result.qualifyingDays, 3.0, 'Sollte 3.0 qualifizierende Tage haben'); - t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen (3-2)'); - t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (1×450€)'); + t.assertEqual(result.qualifyingDaysPaid, 2.0, 'Sollte 2.0 Tage bezahlen (3-1)'); + t.assertEqual(result.totalBonus, 900, 'Bonus sollte 900€ sein (2×450€)'); }); runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => { @@ -261,10 +261,10 @@ runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => { t.assertEqual(result.normalDays, 2.0, 'Sollte 2.0 normale Tage haben'); t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertEqual(result.normalDaysPaid, 2.0, 'Sollte 2.0 normale Tage bezahlen'); - t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 qualifizierenden Tag bezahlen'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 qualifizierenden Tag bezahlen'); t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€'); - t.assertEqual(result.bonusQualifyingDays, 0, 'WE-Tage: 0×450€ = 0€'); - t.assertEqual(result.totalBonus, 500, 'Gesamt: 500€'); + t.assertEqual(result.bonusQualifyingDays, 450, 'WE-Tage: 1×450€ = 450€'); + t.assertEqual(result.totalBonus, 950, 'Gesamt: 950€'); }); runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => { @@ -282,10 +282,10 @@ runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => { t.assertEqual(result.normalDays, 0.5, 'Sollte 0.5 normale Tage haben'); t.assertEqual(result.qualifyingDays, 2.5, 'Sollte 2.5 qualifizierende Tage haben'); - t.assertEqual(result.qualifyingDaysPaid, 0.5, 'Sollte 0.5 qualifizierende Tage bezahlen'); + t.assertEqual(result.qualifyingDaysPaid, 1.5, 'Sollte 1.5 qualifizierende Tage bezahlen'); t.assertEqual(result.bonusNormalDays, 125, 'Normale Tage: 0.5×250€ = 125€'); - t.assertEqual(result.bonusQualifyingDays, 225, 'WE-Tage: 0.5×450€ = 225€'); - t.assertEqual(result.totalBonus, 350, 'Gesamt: 350€'); + t.assertEqual(result.bonusQualifyingDays, 675, 'WE-Tage: 1.5×450€ = 675€'); + t.assertEqual(result.totalBonus, 800, 'Gesamt: 800€'); }); runner.test('Berechnung: Feiertag + Vortag', (t) => { @@ -301,7 +301,7 @@ runner.test('Berechnung: Feiertag + Vortag', (t) => { t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (2.0 - 1.0 = 1.0 × 450€)'); }); runner.test('Berechnung: Keine Dienste = 0€', (t) => { From f5d300bfed56f84a974577a3f2f305c27b4bf4d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:06:28 +0000 Subject: [PATCH 3/7] Improve bonus report text format for cleaner output Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- Dienstplan_Portable.html | 21 ++++++++++++--------- webapp/app.js | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Dienstplan_Portable.html b/Dienstplan_Portable.html index 5fa79f0..0d8931f 100644 --- a/Dienstplan_Portable.html +++ b/Dienstplan_Portable.html @@ -1296,20 +1296,23 @@ class DienstplanApp { totalBonus += bonus; - // Generate note for this employee + // Generate note for this employee - cleaner, more professional format const safeName = this.sanitizeName(name); - let note = `${safeName}: `; + let note = ''; if (!thresholdReached) { - note += `Erreicht das Bonussystem nicht (nur ${we_total.toFixed(1)} WE-Einheiten, mind. 2,0 erforderlich).`; + note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von 2,0 WE-Einheiten) und erhält daher keine Bonuszahlung.`; } else { - const details = []; - if (data.wt > 0) details.push(`${data.wt.toFixed(1)} WT × 250€`); - if (data.we_fr > 0 || data.we_other > 0) { - const paid_we = we_total - 1.0; - details.push(`${paid_we.toFixed(1)} WE × 450€ (abzgl. 1,0 Abzug von ${deductedFrom})`); + const paid_we = we_total - 1.0; + let breakdown = []; + if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à 250 €`); + if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à 450 €`); + + note = `${safeName} erhält eine Bonuszahlung von ${this.formatCurrency(bonus)}`; + if (breakdown.length > 0) { + note += ` (${breakdown.join(' + ')})`; } - note += `Erhält ${this.formatCurrency(bonus)}. ${details.join(', ')}.`; + note += '.'; } employeeNotes.push(note); diff --git a/webapp/app.js b/webapp/app.js index 45a3519..dd97592 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -757,20 +757,23 @@ class DienstplanApp { totalBonus += bonus; - // Generate note + // Generate note - cleaner, more professional format const safeName = escapeHtml(name); - let note = `${safeName}: `; + let note = ''; if (!thresholdReached) { - note += `Erreicht das Bonussystem nicht (nur ${we_total.toFixed(1)} WE-Einheiten, mind. 2,0 erforderlich).`; + note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von 2,0 WE-Einheiten) und erhält daher keine Bonuszahlung.`; } else { - const details = []; - if (data.wt > 0) details.push(`${data.wt.toFixed(1)} WT × 250€`); - if (data.we_fr > 0 || data.we_other > 0) { - const paid_we = we_total - 1.0; - details.push(`${paid_we.toFixed(1)} WE × 450€ (abzgl. 1,0 Abzug von ${deductedFrom})`); + const paid_we = we_total - 1.0; + let breakdown = []; + if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à 250 €`); + if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à 450 €`); + + note = `${safeName} erhält eine Bonuszahlung von ${this.calculator.formatCurrency(bonus)}`; + if (breakdown.length > 0) { + note += ` (${breakdown.join(' + ')})`; } - note += `Erhält ${this.calculator.formatCurrency(bonus)}. ${details.join(', ')}.`; + note += '.'; } employeeNotes.push(note); From feb9c9f712612de652bf31c97cd46dac648973b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:11:07 +0000 Subject: [PATCH 4/7] Address code review feedback: use constants for deduction values Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- Dienstplan_Portable.html | 8 ++++---- webapp/app.js | 10 +++++----- webapp/calculator.js | 5 +++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Dienstplan_Portable.html b/Dienstplan_Portable.html index 0d8931f..3b84e6c 100644 --- a/Dienstplan_Portable.html +++ b/Dienstplan_Portable.html @@ -1301,12 +1301,12 @@ class DienstplanApp { let note = ''; if (!thresholdReached) { - note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von 2,0 WE-Einheiten) und erhält daher keine Bonuszahlung.`; + note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von ${CONFIG.THRESHOLD.toFixed(1)} WE-Einheiten) und erhält daher keine Bonuszahlung.`; } else { - const paid_we = we_total - 1.0; + const paid_we = we_total - CONFIG.DEDUCTION; let breakdown = []; - if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à 250 €`); - if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à 450 €`); + if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à ${CONFIG.RATE_WT} €`); + if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à ${CONFIG.RATE_WE} €`); note = `${safeName} erhält eine Bonuszahlung von ${this.formatCurrency(bonus)}`; if (breakdown.length > 0) { diff --git a/webapp/app.js b/webapp/app.js index dd97592..b03568f 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -738,7 +738,7 @@ class DienstplanApp { if (thresholdReached) { const wt_pay = data.wt * this.calculator.RATE_NORMAL; - let deduct = 1.0; + let deduct = this.calculator.DEDUCTION_AMOUNT; 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); @@ -762,12 +762,12 @@ class DienstplanApp { let note = ''; if (!thresholdReached) { - note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von 2,0 WE-Einheiten) und erhält daher keine Bonuszahlung.`; + note = `${safeName} erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von ${this.calculator.MIN_QUALIFYING_DAYS.toFixed(1)} WE-Einheiten) und erhält daher keine Bonuszahlung.`; } else { - const paid_we = we_total - 1.0; + const paid_we = we_total - this.calculator.DEDUCTION_AMOUNT; let breakdown = []; - if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à 250 €`); - if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à 450 €`); + if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à ${this.calculator.RATE_NORMAL} €`); + if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à ${this.calculator.RATE_WEEKEND} €`); note = `${safeName} erhält eine Bonuszahlung von ${this.calculator.formatCurrency(bonus)}`; if (breakdown.length > 0) { diff --git a/webapp/calculator.js b/webapp/calculator.js index bc5c787..2dd855b 100644 --- a/webapp/calculator.js +++ b/webapp/calculator.js @@ -8,6 +8,7 @@ class BonusCalculator { this.RATE_NORMAL = 250; // Normal day rate (not weekend/holiday) this.RATE_WEEKEND = 450; // Weekend/holiday rate this.MIN_QUALIFYING_DAYS = 2.0; // Minimum qualifying days to trigger bonus + this.DEDUCTION_AMOUNT = 1.0; // Deduction after reaching threshold } /** @@ -108,8 +109,8 @@ class BonusCalculator { let totalDeduction = 0; if (thresholdReached) { - // Deduct 1.0 qualifying day with Friday priority - totalDeduction = 1.0; + // Deduct qualifying days with Friday priority + totalDeduction = this.DEDUCTION_AMOUNT; // First deduct from Friday deductionFromFriday = Math.min(totalDeduction, qualifyingDaysFriday); From 6ecfd895e149568f8d8387ea837ab779d81f260c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:12:49 +0000 Subject: [PATCH 5/7] Update claude.md documentation to reflect correct 1.0 deduction value Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- claude.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/claude.md b/claude.md index 9fde052..3755eaa 100644 --- a/claude.md +++ b/claude.md @@ -53,11 +53,11 @@ Die ältere Implementierung nutzt eine andere Logik: - **WE-Tag** (Weekend): Fr-So + Feiertag + Vortag Feiertag 2. **Bonusberechnung**: - - **WT-Tage** werden **immer** mit 250€ vergütet + - **WT-Tage** werden bei Erreichen der Schwelle mit 250€ vergütet - **WE-Tage** nur vergütet wenn ≥ 2.0 WE-Einheiten: - Bei Erreichen: 450€ pro WE-Tag - - Dann Abzug von 2.0 WE-Einheiten (Freitag-Priorität) - - Unter Schwellenwert: WE-Dienste = 0€ (nicht als WT vergütet) + - Dann Abzug von 1.0 WE-Einheit (Freitag-Priorität) + - Unter Schwellenwert: Keine Bonuszahlung (weder WE noch WT) ### Wichtiger Unterschied - Beispiel @@ -179,13 +179,13 @@ adb install app/build/outputs/apk/debug/app-debug.apk - 2 × Montag (2.0) - 2 × Samstag (2.0) - Erwartung: - - 2.0 qualifizierende → -2.0 Abzug → 0.0 bezahlt - - Bonus: (2 × 250€) + (0 × 450€) = **500€** + - 2.0 qualifizierende → -1.0 Abzug → 1.0 bezahlt + - Bonus: (2 × 250€) + (1 × 450€) = **950€** ### Testfall 4: Feiertag + Vortag - 1 × Donnerstag vor Karfreitag (qualifizierend!) - 1 × Karfreitag (Feiertag, qualifizierend!) -- Erwartung: 2.0 qualifizierende → -2.0 → 0.0 × 450€ = **0€** +- Erwartung: 2.0 qualifizierende → -1.0 → 1.0 × 450€ = **450€** ## Häufige Anpassungen @@ -203,17 +203,12 @@ this.RATE_WEEKEND = 500; // Statt 450 ``` ### Abzug ändern (Web-App) -Aktuell ist der Abzug fest auf 2.0 kodiert in `webapp/calculator.js`, Zeile 112: +Der Abzug ist als Konstante in `webapp/calculator.js` definiert: ```javascript -qualifyingDaysDeducted = 2.0; +this.DEDUCTION_AMOUNT = 1.0; // Im Constructor ``` -Um dies flexibel zu machen, könnte man hinzufügen: -```javascript -this.DEDUCTION_AMOUNT = 2.0; // Im Constructor -// Dann verwenden: -qualifyingDaysDeducted = this.DEDUCTION_AMOUNT; -``` +Um den Abzugswert zu ändern, einfach diesen Wert anpassen. ## Code-Architektur From db6e95f45ce00cbaa07f29c42895dd38713e631c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:26:34 +0000 Subject: [PATCH 6/7] Revert deduction value to 2.0 across all files as per user feedback Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- CHANGELOG.md | 2 +- README.md | 2 +- SPECIFICATION.md | 6 ++-- android-app/README.md | 2 +- .../dienstplan/nrw/data/PayrollCalculator.kt | 2 +- .../dienstplan/nrw/PayrollCalculatorTest.kt | 26 +++++++------- claude.md | 10 +++--- src/build_template.py | 4 +-- src/calculate.py | 2 +- webapp/README.md | 8 ++--- webapp/TEST_GUIDE.md | 12 +++---- webapp/calculator.js | 2 +- webapp/test-suite.js | 34 +++++++++---------- 13 files changed, 56 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb01259..bb5283e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Added native Android mobile app for duty roster management with the same NRW Var - Same WT-Tag classification - Same compensation rates (WT: 250€, WE: 450€) - Same threshold logic (≥ 2.0 WE units) -- Same deduction rules (1.0 unit, Friday priority) +- Same deduction rules (2.0 units, Friday priority) - Same Variante 2 behavior (no WE compensation below threshold) **Testing:** diff --git a/README.md b/README.md index 1b333c7..69e94b4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Native Android-App für mobiles Dienstplan-Management. Siehe [android-app/README - ✅ Automatische Erkennung von Wochenenden (Fr–So), Feiertagen und Vortagen - ✅ Vergütungslogik: WT 250€, WE 450€ (nur ab Schwelle ≥ 2,0 WE-Einheiten) -- ✅ Abzug 1,0 WE-Einheit (Freitag-Priorität) nach Erreichen der Schwelle +- ✅ Abzug 2,0 WE-Einheiten (Freitag-Priorität) nach Erreichen der Schwelle - ✅ Vorbefüllte Monatsvorlagen mit allen Datumswerten - ✅ Excel-kompatibel (ohne Office 365 Funktionen) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 213d6a5..f135c53 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -4,7 +4,7 @@ Stand: 14.11.2025 (Deutschland) ## Ziel -Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt – inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 1,0 WE-Einheit nach Erreichen der Schwelle. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden ebenfalls nur bei Erreichen der WE-Schwelle vergütet. +Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt – inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 2,0 WE-Einheiten nach Erreichen der Schwelle. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden ebenfalls nur bei Erreichen der WE-Schwelle vergütet. Hinweise: - Region: Deutschland, Bundesland wählbar (steuert Feiertage). @@ -30,7 +30,7 @@ Hinweise: - **WE** (WE-Tag): - Wenn Monats-Summe WE-Einheiten < 2,0 → Auszahlung 0 € für alle WE-Einheiten. - Wenn Monats-Summe WE-Einheiten ≥ 2,0 → Auszahlung 450 €/WE-Einheit, - anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat). + anschließend Abzug genau 2,0 WE-Einheiten (max. 1× pro Person/Monat). - Abzugs-Priorität: zuerst aus Freitag-WE-Einheiten, Rest aus den übrigen WE-Einheiten (Sa/So/Feiertag/Vortag). Chronologie muss nicht nachgebildet werden; es genügt die Priorität nach Kategorie. ### Splits/Anteile @@ -51,7 +51,7 @@ Hinweise: - Satz_WT = 250 - Satz_WE = 450 - WE_Schwelle = 2,0 -- Abzug_nach_WE_Schwelle = 1,0 +- Abzug_nach_WE_Schwelle = 2,0 - BL_Auswahl = Dropdown (z. B. BW, BY, BE, …) - Monat_Auswahl = Datum (erster Tag des Zielmonats, z. B. 01.11.2025) - Variante = 2 (fix auf „streng") diff --git a/android-app/README.md b/android-app/README.md index 7326ceb..062be7d 100644 --- a/android-app/README.md +++ b/android-app/README.md @@ -123,7 +123,7 @@ Edit `PayrollCalculator.kt` and modify the constants: - `RATE_WT`: Weekday rate (default 250€) - `RATE_WE`: Weekend rate (default 450€) - `WE_THRESHOLD`: Threshold for WE compensation (default 2.0) -- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 1.0) +- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 2.0) ### Adding Holidays diff --git a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt index 4269a88..48aafb4 100644 --- a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt +++ b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt @@ -24,7 +24,7 @@ class PayrollCalculator { private const val RATE_WT = 250.0 // Satz_WT private const val RATE_WE = 450.0 // Satz_WE private const val WE_THRESHOLD = 2.0 // WE_Schwelle - private const val DEDUCTION_AFTER_THRESHOLD = 1.0 // Abzug_nach_WE_Schwelle + private const val DEDUCTION_AFTER_THRESHOLD = 2.0 // Abzug_nach_WE_Schwelle private const val TOLERANCE = 0.0001 // For floating-point comparisons } diff --git a/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt b/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt index 0cbef49..6a03b0a 100644 --- a/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt +++ b/android-app/app/src/test/java/com/dienstplan/nrw/PayrollCalculatorTest.kt @@ -52,7 +52,7 @@ class PayrollCalculatorTest { /** * Test Case 2: Exactly at threshold (2.0 WE) - * Expected: WE payout = 450€ (1.0 unit after deduction), threshold reached + * Expected: WE payout = 0€ (0.0 units after 2.0 deduction), threshold reached */ @Test fun testExactlyAtThreshold() { @@ -67,14 +67,14 @@ class PayrollCalculatorTest { val result = results[0] assertEquals(2.0, result.weTotal, 0.001) assertTrue(result.thresholdReached) - assertEquals(1.0, result.deductionTotal, 0.001) - assertEquals(1.0, result.wePaid, 0.001) - assertEquals(450.0, result.payoutWE, 0.001) + assertEquals(2.0, result.deductionTotal, 0.001) + assertEquals(0.0, result.wePaid, 0.001) + assertEquals(0.0, result.payoutWE, 0.001) } /** * Test Case 3: Over threshold (3.5 WE) - * Expected: WE payout = 1125€ (2.5 units after 1.0 deduction) + * Expected: WE payout = 675€ (1.5 units after 2.0 deduction) */ @Test fun testOverThreshold() { @@ -91,8 +91,8 @@ class PayrollCalculatorTest { val result = results[0] assertEquals(3.5, result.weTotal, 0.001) assertTrue(result.thresholdReached) - assertEquals(2.5, result.wePaid, 0.001) - assertEquals(1125.0, result.payoutWE, 0.001) + assertEquals(1.5, result.wePaid, 0.001) + assertEquals(675.0, result.payoutWE, 0.001) } /** @@ -115,9 +115,9 @@ class PayrollCalculatorTest { assertEquals(0.4, result.weFriday, 0.001) assertEquals(1.6, result.weOther, 0.001) assertEquals(0.4, result.deductionFriday, 0.001) // All Friday deducted first - assertEquals(0.6, result.deductionOther, 0.001) // Rest from other (0.6 to reach 1.0 total) - assertEquals(1.0, result.wePaid, 0.001) - assertEquals(450.0, result.payoutWE, 0.001) + assertEquals(1.6, result.deductionOther, 0.001) // Rest from other (1.6 to reach 2.0 total) + assertEquals(0.0, result.wePaid, 0.001) + assertEquals(0.0, result.payoutWE, 0.001) } /** @@ -145,11 +145,11 @@ class PayrollCalculatorTest { assertFalse(resultA.thresholdReached) assertEquals(0.0, resultA.payoutWE, 0.001) - // B: above threshold + // B: above threshold (2.5 WE - 2.0 deduction = 0.5 paid) assertTrue(resultB.thresholdReached) assertEquals(2.5, resultB.weTotal, 0.001) - assertEquals(1.5, resultB.wePaid, 0.001) - assertEquals(675.0, resultB.payoutWE, 0.001) + assertEquals(0.5, resultB.wePaid, 0.001) + assertEquals(225.0, resultB.payoutWE, 0.001) } private fun parseDate(dateString: String): Date { diff --git a/claude.md b/claude.md index 3755eaa..0d18bd2 100644 --- a/claude.md +++ b/claude.md @@ -56,7 +56,7 @@ Die ältere Implementierung nutzt eine andere Logik: - **WT-Tage** werden bei Erreichen der Schwelle mit 250€ vergütet - **WE-Tage** nur vergütet wenn ≥ 2.0 WE-Einheiten: - Bei Erreichen: 450€ pro WE-Tag - - Dann Abzug von 1.0 WE-Einheit (Freitag-Priorität) + - Dann Abzug von 2.0 WE-Einheiten (Freitag-Priorität) - Unter Schwellenwert: Keine Bonuszahlung (weder WE noch WT) ### Wichtiger Unterschied - Beispiel @@ -179,13 +179,13 @@ adb install app/build/outputs/apk/debug/app-debug.apk - 2 × Montag (2.0) - 2 × Samstag (2.0) - Erwartung: - - 2.0 qualifizierende → -1.0 Abzug → 1.0 bezahlt - - Bonus: (2 × 250€) + (1 × 450€) = **950€** + - 2.0 qualifizierende → -2.0 Abzug → 0.0 bezahlt + - Bonus: (2 × 250€) + (0 × 450€) = **500€** ### Testfall 4: Feiertag + Vortag - 1 × Donnerstag vor Karfreitag (qualifizierend!) - 1 × Karfreitag (Feiertag, qualifizierend!) -- Erwartung: 2.0 qualifizierende → -1.0 → 1.0 × 450€ = **450€** +- Erwartung: 2.0 qualifizierende → -2.0 → 0.0 × 450€ = **0€** ## Häufige Anpassungen @@ -205,7 +205,7 @@ this.RATE_WEEKEND = 500; // Statt 450 ### Abzug ändern (Web-App) Der Abzug ist als Konstante in `webapp/calculator.js` definiert: ```javascript -this.DEDUCTION_AMOUNT = 1.0; // Im Constructor +this.DEDUCTION_AMOUNT = 2.0; // Im Constructor ``` Um den Abzugswert zu ändern, einfach diesen Wert anpassen. diff --git a/src/build_template.py b/src/build_template.py index 1e32aff..f9f3cfd 100644 --- a/src/build_template.py +++ b/src/build_template.py @@ -61,7 +61,7 @@ def _populate_readme(ws): rules = [ "WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig).", "Variante 2 (streng): WE werden nur vergütet, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden;", - "dann 450 €/WE und Abzug 1,0 (Freitag zuerst). WT werden bei Erreichen der WE-Schwelle mit 250 € vergütet.", + "dann 450 €/WE und Abzug 2,0 (Freitag zuerst). WT werden bei Erreichen der WE-Schwelle mit 250 € vergütet.", "Splits anteilig. Monat und Bundesland in 'Regeln' wählen.", "", "Schritte:", @@ -83,7 +83,7 @@ def _populate_rules(ws): ("Satz_WT", 250, "Euro für jeden Werktagsdienst (Mo–Do, sofern kein WE-Tag)"), ("Satz_WE", 450, "Euro für jeden WE-Tag (Fr–So, Feiertag, Vortag Feiertag)"), ("WE_Schwelle", 2.0, "Ab dieser WE-Anzahl wird vergütet (sonst 0 €)"), - ("Abzug_nach_WE_Schwelle", 1.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"), + ("Abzug_nach_WE_Schwelle", 2.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"), ("BL_Auswahl", "NRW", "Bundesland (steuert Feiertage)"), ("Monat_Auswahl", date(2025, 11, 1), "Erster Tag des Zielmonats"), ("Variante", 2, "Fix: 2 = streng (WE nur bei Schwelle ≥ 2,0)"), diff --git a/src/calculate.py b/src/calculate.py index 1faa1f5..f2464e7 100644 --- a/src/calculate.py +++ b/src/calculate.py @@ -14,7 +14,7 @@ from collections import defaultdict SATZ_WT = 250 # Euro für Werktag SATZ_WE = 450 # Euro für Wochenende WE_SCHWELLE = 2.0 # Mindestanzahl WE-Dienste für Vergütung -ABZUG = 1.0 # Abzug nach Erreichen der Schwelle +ABZUG = 2.0 # Abzug nach Erreichen der Schwelle def load_holidays(wb): diff --git a/webapp/README.md b/webapp/README.md index bd6db7e..00da5ad 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -21,7 +21,7 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta ### Bonusberechnung 1. **Schwellenwert**: Mindestens **2.0 qualifizierende Tage** im Monat erforderlich -2. **Abzug**: Bei Erreichen des Schwellenwerts wird **1.0 qualifizierender Tag** abgezogen (Freitag-Priorität) +2. **Abzug**: Bei Erreichen des Schwellenwerts werden **2.0 qualifizierende Tage** abgezogen (Freitag-Priorität) 3. **Vergütung**: - Normale Tage: **250€** pro Tag - Qualifizierende Tage (WE/Feiertag): **450€** pro Tag @@ -34,9 +34,9 @@ Mitarbeiter hat im Monat: **Berechnung**: - Qualifizierende Tage: 3.0 (Schwellenwert erreicht ✓) -- Abzug: -1.0 qualifizierender Tag -- Bezahlt: 3 normale Tage + 2 qualifizierende Tage -- **Bonus**: (3 × 250€) + (2 × 450€) = **1.650€** +- Abzug: -2.0 qualifizierende Tage +- Bezahlt: 3 normale Tage + 1 qualifizierender Tag +- **Bonus**: (3 × 250€) + (1 × 450€) = **1.200€** ## Installation & Nutzung diff --git a/webapp/TEST_GUIDE.md b/webapp/TEST_GUIDE.md index 7722ab8..6a43fbf 100644 --- a/webapp/TEST_GUIDE.md +++ b/webapp/TEST_GUIDE.md @@ -84,8 +84,8 @@ Dienste: Erwartung: - Qualifizierende Tage: 2.0 - Schwellenwert: ✅ Erreicht -- Abzug: -1.0 -- Bezahlt: 1.0 × 450€ = 450€ +- Abzug: -2.0 +- Bezahlt: 0.0 × 450€ = 0€ ``` ### Beispiel 2: Gemischte Dienste @@ -96,8 +96,8 @@ Dienste: Erwartung: - Normale Tage: 2.0 × 250€ = 500€ -- Qualifizierende Tage: (2.0 - 1.0) × 450€ = 450€ -- Gesamt: 950€ +- Qualifizierende Tage: (2.0 - 2.0) × 450€ = 0€ +- Gesamt: 500€ ``` ### Beispiel 3: Halbe Dienste @@ -110,8 +110,8 @@ Dienste: Erwartung: - Normale Tage: 0.5 × 250€ = 125€ -- Qualifizierende Tage: (2.5 - 1.0) × 450€ = 675€ -- Gesamt: 800€ +- Qualifizierende Tage: (2.5 - 2.0) × 450€ = 225€ +- Gesamt: 350€ ``` ## Tests erweitern diff --git a/webapp/calculator.js b/webapp/calculator.js index 2dd855b..a3edb2e 100644 --- a/webapp/calculator.js +++ b/webapp/calculator.js @@ -8,7 +8,7 @@ class BonusCalculator { this.RATE_NORMAL = 250; // Normal day rate (not weekend/holiday) this.RATE_WEEKEND = 450; // Weekend/holiday rate this.MIN_QUALIFYING_DAYS = 2.0; // Minimum qualifying days to trigger bonus - this.DEDUCTION_AMOUNT = 1.0; // Deduction after reaching threshold + this.DEDUCTION_AMOUNT = 2.0; // Deduction after reaching threshold } /** diff --git a/webapp/test-suite.js b/webapp/test-suite.js index 9b7272b..420306e 100644 --- a/webapp/test-suite.js +++ b/webapp/test-suite.js @@ -191,7 +191,7 @@ runner.test('Berechnung: Unter Schwellenwert (1.0 WE-Tag) = 0€', (t) => { t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); }); -runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => { +runner.test('Berechnung: Genau 2.0 WE-Tage = 0€', (t) => { const holidays = new HolidayProvider(); const calculator = new BonusCalculator(holidays); @@ -204,12 +204,12 @@ runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => { t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.qualifyingDaysDeducted, 1.0, 'Sollte 1.0 Tag abziehen'); - t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen'); - t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); + t.assertEqual(result.qualifyingDaysDeducted, 2.0, 'Sollte 2.0 Tage abziehen'); + t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen'); + t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); }); -runner.test('Berechnung: 2x halbe WE-Dienste = 450€ (genau Schwelle, nach Abzug 1.0)', (t) => { +runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, nach Abzug 2.0)', (t) => { const holidays = new HolidayProvider(); const calculator = new BonusCalculator(holidays); @@ -224,11 +224,11 @@ runner.test('Berechnung: 2x halbe WE-Dienste = 450€ (genau Schwelle, nach Abzu t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben (4×0.5)'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen nach Abzug'); - t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); + t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen nach Abzug'); + t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); }); -runner.test('Berechnung: 3 WE-Tage = 900€', (t) => { +runner.test('Berechnung: 3 WE-Tage = 450€', (t) => { const holidays = new HolidayProvider(); const calculator = new BonusCalculator(holidays); @@ -241,8 +241,8 @@ runner.test('Berechnung: 3 WE-Tage = 900€', (t) => { const result = calculator.calculateMonthlyBonus(duties); t.assertEqual(result.qualifyingDays, 3.0, 'Sollte 3.0 qualifizierende Tage haben'); - t.assertEqual(result.qualifyingDaysPaid, 2.0, 'Sollte 2.0 Tage bezahlen (3-1)'); - t.assertEqual(result.totalBonus, 900, 'Bonus sollte 900€ sein (2×450€)'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tage bezahlen (3-2)'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (1×450€)'); }); runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => { @@ -261,10 +261,10 @@ runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => { t.assertEqual(result.normalDays, 2.0, 'Sollte 2.0 normale Tage haben'); t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertEqual(result.normalDaysPaid, 2.0, 'Sollte 2.0 normale Tage bezahlen'); - t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 qualifizierenden Tag bezahlen'); + t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 qualifizierende Tage bezahlen'); t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€'); - t.assertEqual(result.bonusQualifyingDays, 450, 'WE-Tage: 1×450€ = 450€'); - t.assertEqual(result.totalBonus, 950, 'Gesamt: 950€'); + t.assertEqual(result.bonusQualifyingDays, 0, 'WE-Tage: 0×450€ = 0€'); + t.assertEqual(result.totalBonus, 500, 'Gesamt: 500€'); }); runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => { @@ -282,10 +282,10 @@ runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => { t.assertEqual(result.normalDays, 0.5, 'Sollte 0.5 normale Tage haben'); t.assertEqual(result.qualifyingDays, 2.5, 'Sollte 2.5 qualifizierende Tage haben'); - t.assertEqual(result.qualifyingDaysPaid, 1.5, 'Sollte 1.5 qualifizierende Tage bezahlen'); + t.assertEqual(result.qualifyingDaysPaid, 0.5, 'Sollte 0.5 qualifizierende Tage bezahlen'); t.assertEqual(result.bonusNormalDays, 125, 'Normale Tage: 0.5×250€ = 125€'); - t.assertEqual(result.bonusQualifyingDays, 675, 'WE-Tage: 1.5×450€ = 675€'); - t.assertEqual(result.totalBonus, 800, 'Gesamt: 800€'); + t.assertEqual(result.bonusQualifyingDays, 225, 'WE-Tage: 0.5×450€ = 225€'); + t.assertEqual(result.totalBonus, 350, 'Gesamt: 350€'); }); runner.test('Berechnung: Feiertag + Vortag', (t) => { @@ -301,7 +301,7 @@ runner.test('Berechnung: Feiertag + Vortag', (t) => { t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); - t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (2.0 - 1.0 = 1.0 × 450€)'); + t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein (2.0 - 2.0 = 0.0 × 450€)'); }); runner.test('Berechnung: Keine Dienste = 0€', (t) => { From af4473f4b9c09ec096d15789c2c27f76779f1d4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:03:54 +0000 Subject: [PATCH 7/7] Fix remaining 1.0 deduction references to 2.0 based on code review feedback Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com> --- Dienstplan_Portable.html | 8 ++++---- README.md | 2 +- SPECIFICATION.md | 18 +++++++++--------- android-app/IMPLEMENTATION_SUMMARY.md | 4 ++-- .../dienstplan/nrw/data/PayrollCalculator.kt | 2 +- webapp/app.js | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Dienstplan_Portable.html b/Dienstplan_Portable.html index 3b84e6c..af9cbb1 100644 --- a/Dienstplan_Portable.html +++ b/Dienstplan_Portable.html @@ -448,7 +448,7 @@

Berechnungsregeln (Variante 2 - Streng)

Schwelle: Gesamter Bonus wird nur gezahlt, wenn WE-Einheiten ≥ 2,0.
- Bei Erreichen: WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 1,0 Einheit Abzug).
+ Bei Erreichen: WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 2,0 Einheiten Abzug).
Unter Schwelle: Keine Auszahlung (weder WT noch WE).
WE-Tage: Fr, Sa, So, Feiertage und Vortage von Feiertagen.

@@ -576,7 +576,7 @@ const CONFIG = { RATE_WT: 250, RATE_WE: 450, THRESHOLD: 2.0, - DEDUCTION: 1.0, + DEDUCTION: 2.0, TOLERANCE: 0.0001 }; @@ -1071,7 +1071,7 @@ class DienstplanApp { 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'; + csv += 'Sätze;"WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 2,0 Abzug)"\n'; // Download CSV file const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); @@ -1373,7 +1373,7 @@ class DienstplanApp {
  • Vergütung bei Erreichen der Schwelle:
  • Unter Schwelle: Keine Bonuszahlung (weder WT noch WE)
  • diff --git a/README.md b/README.md index 69e94b4..4e0e728 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Die Datei landet in `output/Dienstplan_YYYY_MM_NRW.xlsx`. - **WE-Tag**: Fr/Sa/So + Feiertag + Vortag Feiertag - **WT-Tag**: Alle anderen Tage (250 € pro Einheit) -- **WE-Vergütung**: Nur wenn Monatssumme ≥ 2,0 WE-Einheiten → 450 €/Einheit, dann Abzug 1,0 (zuerst von Freitag) +- **WE-Vergütung**: Nur wenn Monatssumme ≥ 2,0 WE-Einheiten → 450 €/Einheit, dann Abzug 2,0 (zuerst von Freitag) - **Unter Schwelle**: WE-Dienste = 0 € (nicht als WT vergütet) Details siehe `SPECIFICATION.md`. diff --git a/SPECIFICATION.md b/SPECIFICATION.md index f135c53..b0a7d7c 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -279,17 +279,17 @@ Beispiel-Formel (als hilfsweise Matrix in Checks): A hat 1,75 WE und 1,0 WT → Auszahlung_WE = 0 €; Auszahlung_WT = 0 €; Auszahlung_Gesamt = 0 €. 2) **Genau Schwelle**: - A hat 2,0 WE (Fr 1,0 + Sa 1,0) → Abzug 1,0 (zuerst Fr) → WE_bezahlt = 1,0 → 450 €. + A hat 2,0 WE (Fr 1,0 + Sa 1,0) → Abzug 2,0 (zuerst Fr) → WE_bezahlt = 0,0 → 0 €. 3) **Über Schwelle ohne Freitag**: - A hat 2,0 WE (nur Sa+So) → Abzug 1,0 aus „Andere" → WE_bezahlt = 1,0 → 450 €. + A hat 2,0 WE (nur Sa+So) → Abzug 2,0 aus „Andere" → WE_bezahlt = 0,0 → 0 €. 4) **Starke Überdeckung**: - A hat 3,5 WE → Abzug 1,0 → WE_bezahlt = 2,5 → 2,5×450 €. + A hat 3,5 WE → Abzug 2,0 → WE_bezahlt = 1,5 → 1,5×450 € = 675 €. 5) **Splits rund um 2,0**: - A hat Fr 0,4 + Sa 0,6 + So 1,0 → Summe 2,0 → Abzug 1,0 - (0,4 von Fr, 0,6 von Andere) → WE_bezahlt = 1,0 → 450 €. + A hat Fr 0,4 + Sa 0,6 + So 1,0 → Summe 2,0 → Abzug 2,0 + (0,4 von Fr, 1,6 von Andere) → WE_bezahlt = 0,0 → 0 €. 6) **Unter Schwelle, nur WE-Tage**: A hat 1,0 WE, 0 WT → Auszahlung_WE = 0 €; Auszahlung_Gesamt = 0 €. @@ -300,9 +300,9 @@ Beispiel-Formel (als hilfsweise Matrix in Checks): ## Edge-Cases und Präzisierungen -- Abzug nur einmal pro Person/Monat (fix 1,0), und nur wenn Schwelle erreicht. +- Abzug nur einmal pro Person/Monat (fix 2,0), und nur wenn Schwelle erreicht. - Der Vortag eines Feiertags ist WE-Tag – unabhängig davon, welcher Wochentag er ist. -- Wenn WE_Freitag < 1,0, wird der restliche Abzug (bis 1,0) von WE_Andere genommen. +- Wenn WE_Freitag < 2,0, wird der restliche Abzug (bis 2,0) von WE_Andere genommen. - Monatswechsel: Daten genau per >=Monat_Auswahl und <=EOMONAT(Monat_Auswahl;0) filtern. - Rundungstoleranz 1e-4 bei Schwelle und Datumssummen (Splits wie 0,33/0,67). - Tabellen-Namen („tblPlan", „tblFeiertage", „tblAuswertung") konsequent verwenden. @@ -324,11 +324,11 @@ Lieferumfang (empfohlen): - 18.11.2025: Korrektur Variante 2: **Gesamter Bonus (WT + WE) wird nur gezahlt, wenn WE_Summe ≥ 2,0**. Unter Schwelle: Auszahlung_Gesamt = 0 € (weder WT noch WE). - 14.11.2025: Umstellung auf Variante 2 (streng). WE-Vergütung nur bei WE_Summe ≥ 2,0, - anschließend Abzug 1,0 (Freitag zuerst). Unterhalb der Schwelle: WE-Auszahlung = 0 €. + anschließend Abzug 2,0 (Freitag zuerst). Unterhalb der Schwelle: WE-Auszahlung = 0 €. - 13.11.2025: Vorversion (Variante 1) mit WE-Auszahlung ab erstem WE-Dienst und Abzug nach Schwelle (ersetzt). ## Kurztext (für Blatt „Regeln" als Readme-Hinweis) -„WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig). Variante 2 (streng): Gesamter Bonus (WT + WE) wird nur gezahlt, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden. Bei Erreichen der Schwelle: WT 250 €/Einheit, WE 450 €/Einheit mit Abzug 1,0 (Freitag zuerst). Unter Schwelle: 0 € Auszahlung. Splits anteilig. Monat und Bundesland oben wählen." +„WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig). Variante 2 (streng): Gesamter Bonus (WT + WE) wird nur gezahlt, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden. Bei Erreichen der Schwelle: WT 250 €/Einheit, WE 450 €/Einheit mit Abzug 2,0 (Freitag zuerst). Unter Schwelle: 0 € Auszahlung. Splits anteilig. Monat und Bundesland oben wählen." — Ende der README — diff --git a/android-app/IMPLEMENTATION_SUMMARY.md b/android-app/IMPLEMENTATION_SUMMARY.md index 55e510f..392424c 100644 --- a/android-app/IMPLEMENTATION_SUMMARY.md +++ b/android-app/IMPLEMENTATION_SUMMARY.md @@ -78,8 +78,8 @@ Implements NRW Variante 2 (streng) rules: #### Unit Tests (PayrollCalculatorTest.kt) Comprehensive test coverage including: 1. **Under threshold test**: 1.75 WE + 1.0 WT → WE payout 0€, WT payout 250€ -2. **Exactly at threshold test**: 2.0 WE → WE payout 450€ (1.0 unit after deduction) -3. **Over threshold test**: 3.5 WE → WE payout 1125€ (2.5 units after deduction) +2. **Exactly at threshold test**: 2.0 WE → WE payout 0€ (0.0 units after 2.0 deduction) +3. **Over threshold test**: 3.5 WE → WE payout 675€ (1.5 units after 2.0 deduction) 4. **Friday deduction priority test**: Verifies deduction comes from Friday first 5. **Multiple employees test**: Separate calculations per employee diff --git a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt index 48aafb4..25ebe44 100644 --- a/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt +++ b/android-app/app/src/main/java/com/dienstplan/nrw/data/PayrollCalculator.kt @@ -14,7 +14,7 @@ import kotlin.math.min * - WT-Tag (Weekday): All other days * - WT compensation: 250€ per unit (only if threshold reached) * - WE compensation: Only paid if monthly total >= 2.0 WE units (threshold) - * - If threshold reached: 450€ per WE unit, then deduct exactly 1.0 WE unit + * - If threshold reached: 450€ per WE unit, then deduct exactly 2.0 WE units * - Deduction priority: Friday first, then other WE days * - Below threshold: 0€ for all shifts (neither WT nor WE) */ diff --git a/webapp/app.js b/webapp/app.js index b03568f..0b5193a 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -526,7 +526,7 @@ class DienstplanApp { 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'; + csv += 'Abzug;"Bei Erreichen der Schwelle werden 2,0 WE-Einheiten abgezogen"\n'; // Download CSV file const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); @@ -835,7 +835,7 @@ class DienstplanApp {
  • Vergütung bei Erreichen der Schwelle:
  • Unter Schwelle: Keine Bonuszahlung (weder WT noch WE)