Merge pull request #26 from Kenearos/copilot/fix-calculations-and-report
Fix deduction value inconsistency (2.0→1.0) and improve bonus report format
This commit is contained in:
commit
96eab8bfad
13 changed files with 67 additions and 64 deletions
|
|
@ -448,7 +448,7 @@
|
||||||
<h4>Berechnungsregeln (Variante 2 - Streng)</h4>
|
<h4>Berechnungsregeln (Variante 2 - Streng)</h4>
|
||||||
<p>
|
<p>
|
||||||
<strong>Schwelle:</strong> Gesamter Bonus wird nur gezahlt, wenn WE-Einheiten ≥ 2,0.<br>
|
<strong>Schwelle:</strong> Gesamter Bonus wird nur gezahlt, wenn WE-Einheiten ≥ 2,0.<br>
|
||||||
<strong>Bei Erreichen:</strong> WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 1,0 Einheit Abzug).<br>
|
<strong>Bei Erreichen:</strong> WT = 250 EUR/Einheit, WE = 450 EUR/Einheit (abzgl. 2,0 Einheiten Abzug).<br>
|
||||||
<strong>Unter Schwelle:</strong> Keine Auszahlung (weder WT noch WE).<br>
|
<strong>Unter Schwelle:</strong> Keine Auszahlung (weder WT noch WE).<br>
|
||||||
<strong>WE-Tage:</strong> Fr, Sa, So, Feiertage und Vortage von Feiertagen.
|
<strong>WE-Tage:</strong> Fr, Sa, So, Feiertage und Vortage von Feiertagen.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -576,7 +576,7 @@ const CONFIG = {
|
||||||
RATE_WT: 250,
|
RATE_WT: 250,
|
||||||
RATE_WE: 450,
|
RATE_WE: 450,
|
||||||
THRESHOLD: 2.0,
|
THRESHOLD: 2.0,
|
||||||
DEDUCTION: 1.0,
|
DEDUCTION: 2.0,
|
||||||
TOLERANCE: 0.0001
|
TOLERANCE: 0.0001
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1071,7 +1071,7 @@ class DienstplanApp {
|
||||||
csv += 'WT;Werktag (Montag-Donnerstag ohne Feiertag/Vortag)\n';
|
csv += 'WT;Werktag (Montag-Donnerstag ohne Feiertag/Vortag)\n';
|
||||||
csv += 'WE-Tag;"Freitag, Samstag, Sonntag, Feiertag oder Tag vor Feiertag"\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 += '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
|
// Download CSV file
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||||
|
|
@ -1296,20 +1296,23 @@ class DienstplanApp {
|
||||||
|
|
||||||
totalBonus += bonus;
|
totalBonus += bonus;
|
||||||
|
|
||||||
// Generate note for this employee
|
// Generate note for this employee - cleaner, more professional format
|
||||||
const safeName = this.sanitizeName(name);
|
const safeName = this.sanitizeName(name);
|
||||||
let note = `<b>${safeName}</b>: `;
|
let note = '';
|
||||||
|
|
||||||
if (!thresholdReached) {
|
if (!thresholdReached) {
|
||||||
note += `Erreicht das Bonussystem nicht (nur ${we_total.toFixed(1)} WE-Einheiten, mind. 2,0 erforderlich).`;
|
note = `<b>${safeName}</b> erreicht die Mindestschwelle nicht (${we_total.toFixed(1)} von ${CONFIG.THRESHOLD.toFixed(1)} WE-Einheiten) und erhält daher keine Bonuszahlung.`;
|
||||||
} else {
|
} else {
|
||||||
const details = [];
|
const paid_we = we_total - CONFIG.DEDUCTION;
|
||||||
if (data.wt > 0) details.push(`${data.wt.toFixed(1)} WT × 250€`);
|
let breakdown = [];
|
||||||
if (data.we_fr > 0 || data.we_other > 0) {
|
if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à ${CONFIG.RATE_WT} €`);
|
||||||
const paid_we = we_total - 1.0;
|
if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à ${CONFIG.RATE_WE} €`);
|
||||||
details.push(`${paid_we.toFixed(1)} WE × 450€ (abzgl. 1,0 Abzug von ${deductedFrom})`);
|
|
||||||
|
note = `<b>${safeName}</b> erhält eine Bonuszahlung von <span style="color: #28a745; font-weight: bold;">${this.formatCurrency(bonus)}</span>`;
|
||||||
|
if (breakdown.length > 0) {
|
||||||
|
note += ` (${breakdown.join(' + ')})`;
|
||||||
}
|
}
|
||||||
note += `Erhält ${this.formatCurrency(bonus)}. ${details.join(', ')}.`;
|
note += '.';
|
||||||
}
|
}
|
||||||
employeeNotes.push(note);
|
employeeNotes.push(note);
|
||||||
|
|
||||||
|
|
@ -1370,7 +1373,7 @@ class DienstplanApp {
|
||||||
<li><strong>Vergütung bei Erreichen der Schwelle:</strong>
|
<li><strong>Vergütung bei Erreichen der Schwelle:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Werktage (WT): 250 € pro Einheit</li>
|
<li>Werktage (WT): 250 € pro Einheit</li>
|
||||||
<li>WE-Tage: 450 € pro Einheit (abzüglich 1,0 Einheit Abzug, Freitag zuerst)</li>
|
<li>WE-Tage: 450 € pro Einheit (abzüglich 2,0 Einheiten Abzug, Freitag zuerst)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><strong>Unter Schwelle:</strong> Keine Bonuszahlung (weder WT noch WE)</li>
|
<li><strong>Unter Schwelle:</strong> Keine Bonuszahlung (weder WT noch WE)</li>
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ Die Datei landet in `output/Dienstplan_YYYY_MM_NRW.xlsx`.
|
||||||
|
|
||||||
- **WE-Tag**: Fr/Sa/So + Feiertag + Vortag Feiertag
|
- **WE-Tag**: Fr/Sa/So + Feiertag + Vortag Feiertag
|
||||||
- **WT-Tag**: Alle anderen Tage (250 € pro Einheit)
|
- **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)
|
- **Unter Schwelle**: WE-Dienste = 0 € (nicht als WT vergütet)
|
||||||
|
|
||||||
Details siehe `SPECIFICATION.md`.
|
Details siehe `SPECIFICATION.md`.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Stand: 14.11.2025 (Deutschland)
|
||||||
|
|
||||||
## Ziel
|
## 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 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:
|
Hinweise:
|
||||||
- Region: Deutschland, Bundesland wählbar (steuert Feiertage).
|
- Region: Deutschland, Bundesland wählbar (steuert Feiertage).
|
||||||
|
|
@ -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 €.
|
A hat 1,75 WE und 1,0 WT → Auszahlung_WE = 0 €; Auszahlung_WT = 0 €; Auszahlung_Gesamt = 0 €.
|
||||||
|
|
||||||
2) **Genau Schwelle**:
|
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**:
|
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**:
|
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**:
|
5) **Splits rund um 2,0**:
|
||||||
A hat Fr 0,4 + Sa 0,6 + So 1,0 → Summe 2,0 → Abzug 1,0
|
A hat Fr 0,4 + Sa 0,6 + So 1,0 → Summe 2,0 → Abzug 2,0
|
||||||
(0,4 von Fr, 0,6 von Andere) → WE_bezahlt = 1,0 → 450 €.
|
(0,4 von Fr, 1,6 von Andere) → WE_bezahlt = 0,0 → 0 €.
|
||||||
|
|
||||||
6) **Unter Schwelle, nur WE-Tage**:
|
6) **Unter Schwelle, nur WE-Tage**:
|
||||||
A hat 1,0 WE, 0 WT → Auszahlung_WE = 0 €; Auszahlung_Gesamt = 0 €.
|
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
|
## 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.
|
- 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.
|
- 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).
|
- Rundungstoleranz 1e-4 bei Schwelle und Datumssummen (Splits wie 0,33/0,67).
|
||||||
- Tabellen-Namen („tblPlan", „tblFeiertage", „tblAuswertung") konsequent verwenden.
|
- 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**.
|
- 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).
|
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,
|
- 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).
|
- 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)
|
## 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 —
|
— Ende der README —
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@ Implements NRW Variante 2 (streng) rules:
|
||||||
#### Unit Tests (PayrollCalculatorTest.kt)
|
#### Unit Tests (PayrollCalculatorTest.kt)
|
||||||
Comprehensive test coverage including:
|
Comprehensive test coverage including:
|
||||||
1. **Under threshold test**: 1.75 WE + 1.0 WT → WE payout 0€, WT payout 250€
|
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)
|
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 1125€ (2.5 units after 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
|
4. **Friday deduction priority test**: Verifies deduction comes from Friday first
|
||||||
5. **Multiple employees test**: Separate calculations per employee
|
5. **Multiple employees test**: Separate calculations per employee
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import kotlin.math.min
|
||||||
* Business rules:
|
* Business rules:
|
||||||
* - WE-Tag (Weekend/Holiday): Friday, Saturday, Sunday, public holiday, day before public holiday
|
* - WE-Tag (Weekend/Holiday): Friday, Saturday, Sunday, public holiday, day before public holiday
|
||||||
* - WT-Tag (Weekday): All other days
|
* - 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)
|
* - 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 2.0 WE units
|
||||||
* - Deduction priority: Friday first, then other WE days
|
* - 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 {
|
class PayrollCalculator {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class PayrollCalculatorTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Case 2: Exactly at threshold (2.0 WE)
|
* Test Case 2: Exactly at threshold (2.0 WE)
|
||||||
* Expected: WE payout = 0€ (0.0 units after deduction), threshold reached
|
* Expected: WE payout = 0€ (0.0 units after 2.0 deduction), threshold reached
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testExactlyAtThreshold() {
|
fun testExactlyAtThreshold() {
|
||||||
|
|
@ -74,7 +74,7 @@ class PayrollCalculatorTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Case 3: Over threshold (3.5 WE)
|
* Test Case 3: Over threshold (3.5 WE)
|
||||||
* Expected: WE payout = 675€ (1.5 units after deduction)
|
* Expected: WE payout = 675€ (1.5 units after 2.0 deduction)
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testOverThreshold() {
|
fun testOverThreshold() {
|
||||||
|
|
@ -117,6 +117,7 @@ class PayrollCalculatorTest {
|
||||||
assertEquals(0.4, result.deductionFriday, 0.001) // All Friday deducted first
|
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(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.wePaid, 0.001)
|
||||||
|
assertEquals(0.0, result.payoutWE, 0.001)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,7 +145,7 @@ class PayrollCalculatorTest {
|
||||||
assertFalse(resultA.thresholdReached)
|
assertFalse(resultA.thresholdReached)
|
||||||
assertEquals(0.0, resultA.payoutWE, 0.001)
|
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)
|
assertTrue(resultB.thresholdReached)
|
||||||
assertEquals(2.5, resultB.weTotal, 0.001)
|
assertEquals(2.5, resultB.weTotal, 0.001)
|
||||||
assertEquals(0.5, resultB.wePaid, 0.001)
|
assertEquals(0.5, resultB.wePaid, 0.001)
|
||||||
|
|
|
||||||
15
claude.md
15
claude.md
|
|
@ -53,11 +53,11 @@ Die ältere Implementierung nutzt eine andere Logik:
|
||||||
- **WE-Tag** (Weekend): Fr-So + Feiertag + Vortag Feiertag
|
- **WE-Tag** (Weekend): Fr-So + Feiertag + Vortag Feiertag
|
||||||
|
|
||||||
2. **Bonusberechnung**:
|
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:
|
- **WE-Tage** nur vergütet wenn ≥ 2.0 WE-Einheiten:
|
||||||
- Bei Erreichen: 450€ pro WE-Tag
|
- Bei Erreichen: 450€ pro WE-Tag
|
||||||
- Dann Abzug von 2.0 WE-Einheiten (Freitag-Priorität)
|
- Dann Abzug von 2.0 WE-Einheiten (Freitag-Priorität)
|
||||||
- Unter Schwellenwert: WE-Dienste = 0€ (nicht als WT vergütet)
|
- Unter Schwellenwert: Keine Bonuszahlung (weder WE noch WT)
|
||||||
|
|
||||||
### Wichtiger Unterschied - Beispiel
|
### Wichtiger Unterschied - Beispiel
|
||||||
|
|
||||||
|
|
@ -203,18 +203,13 @@ this.RATE_WEEKEND = 500; // Statt 450
|
||||||
```
|
```
|
||||||
|
|
||||||
### Abzug ändern (Web-App)
|
### 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;
|
|
||||||
```
|
|
||||||
|
|
||||||
Um dies flexibel zu machen, könnte man hinzufügen:
|
|
||||||
```javascript
|
```javascript
|
||||||
this.DEDUCTION_AMOUNT = 2.0; // Im Constructor
|
this.DEDUCTION_AMOUNT = 2.0; // Im Constructor
|
||||||
// Dann verwenden:
|
|
||||||
qualifyingDaysDeducted = this.DEDUCTION_AMOUNT;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Um den Abzugswert zu ändern, einfach diesen Wert anpassen.
|
||||||
|
|
||||||
## Code-Architektur
|
## Code-Architektur
|
||||||
|
|
||||||
### Web-App (MVC-ähnlich)
|
### Web-App (MVC-ähnlich)
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ def _populate_readme(ws):
|
||||||
rules = [
|
rules = [
|
||||||
"WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig).",
|
"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;",
|
"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 2,0 (Freitag zuerst). WT werden bei Erreichen der WE-Schwelle mit 250 € vergütet.",
|
||||||
"Splits anteilig. Monat und Bundesland in 'Regeln' wählen.",
|
"Splits anteilig. Monat und Bundesland in 'Regeln' wählen.",
|
||||||
"",
|
"",
|
||||||
"Schritte:",
|
"Schritte:",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta
|
||||||
|
|
||||||
### Bonusberechnung
|
### Bonusberechnung
|
||||||
1. **Schwellenwert**: Mindestens **2.0 qualifizierende Tage** im Monat erforderlich
|
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 werden **2.0 qualifizierende Tage** abgezogen (Freitag-Priorität)
|
||||||
3. **Vergütung**:
|
3. **Vergütung**:
|
||||||
- Normale Tage: **250€** pro Tag
|
- Normale Tage: **250€** pro Tag
|
||||||
- Qualifizierende Tage (WE/Feiertag): **450€** pro Tag
|
- Qualifizierende Tage (WE/Feiertag): **450€** pro Tag
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,8 @@ Dienste:
|
||||||
|
|
||||||
Erwartung:
|
Erwartung:
|
||||||
- Normale Tage: 0.5 × 250€ = 125€
|
- Normale Tage: 0.5 × 250€ = 125€
|
||||||
- Qualifizierende Tage: (2.5 - 1.0) × 450€ = 675€
|
- Qualifizierende Tage: (2.5 - 2.0) × 450€ = 225€
|
||||||
- Gesamt: 800€
|
- Gesamt: 350€
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests erweitern
|
## Tests erweitern
|
||||||
|
|
|
||||||
|
|
@ -526,7 +526,7 @@ class DienstplanApp {
|
||||||
csv += 'WE/Feiertag Tage;"Freitag, Samstag, Sonntag, Feiertag oder Tag vor Feiertag"\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 += '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 += '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
|
// Download CSV file
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||||
|
|
@ -738,7 +738,7 @@ class DienstplanApp {
|
||||||
|
|
||||||
if (thresholdReached) {
|
if (thresholdReached) {
|
||||||
const wt_pay = data.wt * this.calculator.RATE_NORMAL;
|
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_fr = Math.min(deduct, data.we_fr);
|
||||||
const deduct_other = Math.max(0, deduct - deduct_fr);
|
const deduct_other = Math.max(0, deduct - deduct_fr);
|
||||||
const paid_fr = Math.max(0, data.we_fr - deduct_fr);
|
const paid_fr = Math.max(0, data.we_fr - deduct_fr);
|
||||||
|
|
@ -757,20 +757,23 @@ class DienstplanApp {
|
||||||
|
|
||||||
totalBonus += bonus;
|
totalBonus += bonus;
|
||||||
|
|
||||||
// Generate note
|
// Generate note - cleaner, more professional format
|
||||||
const safeName = escapeHtml(name);
|
const safeName = escapeHtml(name);
|
||||||
let note = `<b>${safeName}</b>: `;
|
let note = '';
|
||||||
|
|
||||||
if (!thresholdReached) {
|
if (!thresholdReached) {
|
||||||
note += `Erreicht das Bonussystem nicht (nur ${we_total.toFixed(1)} WE-Einheiten, mind. 2,0 erforderlich).`;
|
note = `<b>${safeName}</b> 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 {
|
} else {
|
||||||
const details = [];
|
const paid_we = we_total - this.calculator.DEDUCTION_AMOUNT;
|
||||||
if (data.wt > 0) details.push(`${data.wt.toFixed(1)} WT × 250€`);
|
let breakdown = [];
|
||||||
if (data.we_fr > 0 || data.we_other > 0) {
|
if (data.wt > 0) breakdown.push(`${data.wt.toFixed(1)} WT-Einheiten à ${this.calculator.RATE_NORMAL} €`);
|
||||||
const paid_we = we_total - 1.0;
|
if (paid_we > 0) breakdown.push(`${paid_we.toFixed(1)} WE-Einheiten à ${this.calculator.RATE_WEEKEND} €`);
|
||||||
details.push(`${paid_we.toFixed(1)} WE × 450€ (abzgl. 1,0 Abzug von ${deductedFrom})`);
|
|
||||||
|
note = `<b>${safeName}</b> erhält eine Bonuszahlung von <span style="color: #28a745; font-weight: bold;">${this.calculator.formatCurrency(bonus)}</span>`;
|
||||||
|
if (breakdown.length > 0) {
|
||||||
|
note += ` (${breakdown.join(' + ')})`;
|
||||||
}
|
}
|
||||||
note += `Erhält ${this.calculator.formatCurrency(bonus)}. ${details.join(', ')}.`;
|
note += '.';
|
||||||
}
|
}
|
||||||
employeeNotes.push(note);
|
employeeNotes.push(note);
|
||||||
|
|
||||||
|
|
@ -832,7 +835,7 @@ class DienstplanApp {
|
||||||
<li><strong>Vergütung bei Erreichen der Schwelle:</strong>
|
<li><strong>Vergütung bei Erreichen der Schwelle:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Werktage (WT): 250 € pro Einheit</li>
|
<li>Werktage (WT): 250 € pro Einheit</li>
|
||||||
<li>WE-Tage: 450 € pro Einheit (abzüglich 1,0 Einheit Abzug, Freitag zuerst)</li>
|
<li>WE-Tage: 450 € pro Einheit (abzüglich 2,0 Einheiten Abzug, Freitag zuerst)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><strong>Unter Schwelle:</strong> Keine Bonuszahlung (weder WT noch WE)</li>
|
<li><strong>Unter Schwelle:</strong> Keine Bonuszahlung (weder WT noch WE)</li>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class BonusCalculator {
|
||||||
this.RATE_NORMAL = 250; // Normal day rate (not weekend/holiday)
|
this.RATE_NORMAL = 250; // Normal day rate (not weekend/holiday)
|
||||||
this.RATE_WEEKEND = 450; // Weekend/holiday rate
|
this.RATE_WEEKEND = 450; // Weekend/holiday rate
|
||||||
this.MIN_QUALIFYING_DAYS = 2.0; // Minimum qualifying days to trigger bonus
|
this.MIN_QUALIFYING_DAYS = 2.0; // Minimum qualifying days to trigger bonus
|
||||||
|
this.DEDUCTION_AMOUNT = 2.0; // Deduction after reaching threshold
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,8 +109,8 @@ class BonusCalculator {
|
||||||
let totalDeduction = 0;
|
let totalDeduction = 0;
|
||||||
|
|
||||||
if (thresholdReached) {
|
if (thresholdReached) {
|
||||||
// Deduct 2.0 qualifying days with Friday priority
|
// Deduct qualifying days with Friday priority
|
||||||
totalDeduction = 2.0;
|
totalDeduction = this.DEDUCTION_AMOUNT;
|
||||||
|
|
||||||
// First deduct from Friday
|
// First deduct from Friday
|
||||||
deductionFromFriday = Math.min(totalDeduction, qualifyingDaysFriday);
|
deductionFromFriday = Math.min(totalDeduction, qualifyingDaysFriday);
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ runner.test('Berechnung: Unter Schwellenwert (1.0 WE-Tag) = 0€', (t) => {
|
||||||
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
|
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 holidays = new HolidayProvider();
|
||||||
const calculator = new BonusCalculator(holidays);
|
const calculator = new BonusCalculator(holidays);
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => {
|
||||||
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
|
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, aber nach Abzug nichts)', (t) => {
|
runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, nach Abzug 2.0)', (t) => {
|
||||||
const holidays = new HolidayProvider();
|
const holidays = new HolidayProvider();
|
||||||
const calculator = new BonusCalculator(holidays);
|
const calculator = new BonusCalculator(holidays);
|
||||||
|
|
||||||
|
|
@ -241,7 +241,7 @@ runner.test('Berechnung: 3 WE-Tage = 450€', (t) => {
|
||||||
const result = calculator.calculateMonthlyBonus(duties);
|
const result = calculator.calculateMonthlyBonus(duties);
|
||||||
|
|
||||||
t.assertEqual(result.qualifyingDays, 3.0, 'Sollte 3.0 qualifizierende Tage haben');
|
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.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tage bezahlen (3-2)');
|
||||||
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (1×450€)');
|
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (1×450€)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -261,7 +261,7 @@ runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => {
|
||||||
t.assertEqual(result.normalDays, 2.0, 'Sollte 2.0 normale Tage haben');
|
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.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
|
||||||
t.assertEqual(result.normalDaysPaid, 2.0, 'Sollte 2.0 normale Tage bezahlen');
|
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, 0.0, 'Sollte 0.0 qualifizierende Tage bezahlen');
|
||||||
t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€');
|
t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€');
|
||||||
t.assertEqual(result.bonusQualifyingDays, 0, 'WE-Tage: 0×450€ = 0€');
|
t.assertEqual(result.bonusQualifyingDays, 0, 'WE-Tage: 0×450€ = 0€');
|
||||||
t.assertEqual(result.totalBonus, 500, 'Gesamt: 500€');
|
t.assertEqual(result.totalBonus, 500, 'Gesamt: 500€');
|
||||||
|
|
@ -301,7 +301,7 @@ runner.test('Berechnung: Feiertag + Vortag', (t) => {
|
||||||
|
|
||||||
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
|
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
|
||||||
t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein');
|
t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein');
|
||||||
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
|
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein (2.0 - 2.0 = 0.0 × 450€)');
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.test('Berechnung: Keine Dienste = 0€', (t) => {
|
runner.test('Berechnung: Keine Dienste = 0€', (t) => {
|
||||||
|
|
|
||||||
Reference in a new issue