diff --git a/app.js b/app.js
index b50f8dd..01d6aa7 100644
--- a/app.js
+++ b/app.js
@@ -508,8 +508,13 @@ class DienstplanApp {
const year = parseInt(yearSelect.value);
const employeeDuties = this.storage.getAllEmployeeDutiesForMonth(year, month);
- const results = this.calculator.calculateAllEmployees(employeeDuties);
-
+ const yearMonth = `${year}-${String(month).padStart(2, '0')}`;
+ const vacationMap = {};
+ Object.keys(employeeDuties).forEach(n => {
+ vacationMap[n] = this.storage.getVacationMode(n, yearMonth);
+ });
+ const results = this.calculator.calculateAllEmployees(employeeDuties, vacationMap);
+
const monthName = this.getMonthName(month);
let reportHtml = `
Dienstplan Abrechnung ${monthName} ${year}
`;
@@ -530,25 +535,26 @@ class DienstplanApp {
if (results && Object.keys(results).length > 0) {
Object.keys(results).forEach(name => {
const res = results[name];
- const totalWe = res.qualifyingDays || 0;
- const deducted = res.qualifyingDaysDeducted || 0;
- const threshold = res.thresholdReached;
-
- let statusText = "";
- let rowStyle = "";
- let blockText = "";
+ const w = res.winner;
+ const c = res.classified;
+ const totalWe = c.fr + c.sa + c.so;
+ const deducted = w.deduction.fr + w.deduction.sa + w.deduction.so;
+ const triggered = w.eligible && res.totalBonus > 0;
- if (threshold) {
- statusText = "Variante 3 (Bonus)";
- rowStyle = "";
- blockText = `Herr/Frau ${name} erreicht ${this.formatNumber(totalWe)} Wochenenddienste, es werden ihm/ihr ${this.formatNumber(deducted)} Wochenenddienste nicht angerechnet und somit erreicht er/sie Variante 3.`;
- } else if (totalWe > 0) {
- statusText = "Bonus nicht erreicht";
- rowStyle = "background-color: #fff0f0;";
- blockText = `Mitarbeiter ${name} erreicht das Bonussystem nicht (${this.formatNumber(totalWe)} WE-Dienste < 2.0).`;
+ let statusText = '';
+ let rowStyle = '';
+ let blockText = '';
+
+ if (triggered) {
+ statusText = `Variante ${w.variantId} (${this.calculator.formatCurrency(res.totalBonus)})${res.isVacation ? ' - Urlaub' : ''}`;
+ blockText = `Herr/Frau ${name} erreicht ${this.formatNumber(totalWe)} qualifizierende Dienste (Fr/Sa/So), ${this.formatNumber(deducted)} davon werden abgezogen - Bonus nach Variante ${w.variantId}: ${this.calculator.formatCurrency(res.totalBonus)}${res.isVacation ? ' (Urlaubsmodus aktiv)' : ''}.`;
+ } else if (totalWe > 0 || c.weekday > 0) {
+ statusText = 'Bonus nicht erreicht';
+ rowStyle = 'background-color: #fff0f0;';
+ blockText = `Mitarbeiter ${name} erreicht in keiner der drei Varianten die Schwelle (Fr ${c.fr.toFixed(1)}, Sa ${c.sa.toFixed(1)}, So ${c.so.toFixed(1)}, Werktage ${c.weekday.toFixed(1)})${res.isVacation ? ' - Urlaubsmodus aktiv' : ''}.`;
} else {
- statusText = "-";
- rowStyle = "color: #999;";
+ statusText = '-';
+ rowStyle = 'color: #999;';
}
reportHtml += `
@@ -692,39 +698,47 @@ class DienstplanApp {
// === 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';
-
+ csv += 'Mitarbeiter;Urlaub;Sieger-Variante;Fr;Sa;So;Werktage;Eligible;Abzug Fr;Abzug Sa;Abzug So;Abzug WT;Bonus (EUR)\n';
+
+ const yearMonth = `${year}-${String(month).padStart(2, '0')}`;
const employeeDuties = this.storage.getAllEmployeeDutiesForMonth(year, month);
- const results = this.calculator.calculateAllEmployees(employeeDuties);
-
+ const vacationMap = {};
+ Object.keys(employeeDuties).forEach(name => {
+ vacationMap[name] = this.storage.getVacationMode(name, yearMonth);
+ });
+ const results = this.calculator.calculateAllEmployees(employeeDuties, vacationMap);
+
let totalBonus = 0;
-
for (const [employeeName, result] of Object.entries(results)) {
- const threshold = result.thresholdReached ? 'JA' : 'NEIN';
-
+ const w = result.winner;
+ const c = result.classified;
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.isVacation ? 'JA' : 'NEIN'};`;
+ csv += `V${w.variantId};`;
+ csv += `${c.fr.toFixed(1).replace('.', ',')};`;
+ csv += `${c.sa.toFixed(1).replace('.', ',')};`;
+ csv += `${c.so.toFixed(1).replace('.', ',')};`;
+ csv += `${c.weekday.toFixed(1).replace('.', ',')};`;
+ csv += `${w.eligible ? 'JA' : 'NEIN'};`;
+ csv += `${w.deduction.fr.toFixed(2).replace('.', ',')};`;
+ csv += `${w.deduction.sa.toFixed(2).replace('.', ',')};`;
+ csv += `${w.deduction.so.toFixed(2).replace('.', ',')};`;
+ csv += `${w.deduction.weekday.toFixed(2).replace('.', ',')};`;
csv += `${result.totalBonus.toFixed(2).replace('.', ',')}\n`;
}
-
- csv += `\nGESAMT;;;;;;;;;${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 werden 2,0 WE-Einheiten abgezogen"\n';
+ csv += 'Fr/Sa/So/Werktage;Klassifizierte Shares pro Slot (Halbdienste 0,5)\n';
+ csv += 'Sieger-Variante;V1, V2 oder V3 - automatisch die Variante mit dem höchsten Bonus\n';
+ csv += 'V1;"fr+so >= 1 UND weekday >= 3 (Halbiert bei Urlaub: 0,5 / 1,5)"\n';
+ csv += 'V2;"sa >= 1 UND weekday >= 2 (Halbiert bei Urlaub: 0,5 / 1)"\n';
+ csv += 'V3 (loose);"fr+sa+so >= 2 - wie bisher (Halbiert bei Urlaub: 1)"\n';
+ csv += 'Urlaub;"Wenn JA: Schwellen und Abzüge halbiert"\n';
+ csv += 'Sätze;"Werktag = 250 EUR/Einheit, Fr/Sa/So/Feiertag = 450 EUR/Einheit"\n';
// Download CSV file
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
@@ -776,30 +790,18 @@ class DienstplanApp {
for (const [name, duties] of Object.entries(employeeDuties)) {
employeeData[name] = {
duties: duties,
- byWeekday: { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] },
- wt: 0,
- we_fr: 0,
- we_other: 0
+ byWeekday: { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] }
};
-
+
duties.forEach(duty => {
const dayOfWeek = duty.date.getDay();
const isQualifying = this.calculator.isQualifyingDay(duty.date);
- const isFriday = dayOfWeek === 5;
-
+
employeeData[name].byWeekday[dayOfWeek].push({
...duty,
isQual: isQualifying,
dayType: this.calculator.getDayTypeLabel(duty.date)
});
-
- if (!isQualifying) {
- employeeData[name].wt += duty.share;
- } else if (isFriday) {
- employeeData[name].we_fr += duty.share;
- } else {
- employeeData[name].we_other += duty.share;
- }
});
}
@@ -934,54 +936,37 @@ class DienstplanApp {
let totalBonus = 0;
const employeeNotes = [];
-
+
+ // Compute via BonusCalculator (uses winning variant)
+ const yearMonth = `${year}-${String(month).padStart(2, '0')}`;
+ const vacationMap = {};
+ Object.keys(employeeDuties).forEach(n => {
+ vacationMap[n] = this.storage.getVacationMode(n, yearMonth);
+ });
+ const calcResults = this.calculator.calculateAllEmployees(employeeDuties, vacationMap);
+
for (const [name, data] of Object.entries(employeeData)) {
- const we_total = data.we_fr + data.we_other;
- const thresholdReached = we_total >= this.calculator.MIN_QUALIFYING_DAYS - 0.0001;
-
- let bonus = 0;
-
- if (thresholdReached) {
- const wt_pay = data.wt * this.calculator.RATE_NORMAL;
- let deduct = this.calculator.DEDUCTION_AMOUNT;
- let deduct_fr = Math.min(deduct, data.we_fr);
- let 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) * this.calculator.RATE_WEEKEND;
- bonus = wt_pay + we_pay;
- }
-
+ const calcRes = calcResults[name] || this.calculator.getEmptyResult();
+ const bonus = calcRes.totalBonus;
+ const w = calcRes.winner;
+
totalBonus += bonus;
-
- // Generate note - cleaner, more professional format
+
const safeName = escapeHtml(name);
let note = '';
-
- if (!thresholdReached) {
- 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.`;
+ if (bonus === 0 || !w.eligible) {
+ note = `${safeName} erreicht in keiner der drei Varianten einen positiven Bonus${calcRes.isVacation ? ' (Urlaubsmodus aktiv)' : ''} und erhält daher keine Bonuszahlung.`;
} else {
- const paid_we = we_total - this.calculator.DEDUCTION_AMOUNT;
- let breakdown = [];
- 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) {
- note += ` (${breakdown.join(' + ')})`;
- }
- note += '.';
+ const c = calcRes.classified;
+ note = `${safeName} erhält eine Bonuszahlung von ${this.calculator.formatCurrency(bonus)} nach Variante ${w.variantId}${calcRes.isVacation ? ' (Urlaubsmodus aktiv)' : ''}. Klassifiziert: Fr ${c.fr.toFixed(1)} / Sa ${c.sa.toFixed(1)} / So ${c.so.toFixed(1)} / Werktage ${c.weekday.toFixed(1)}.`;
}
employeeNotes.push(note);
-
+
// Build table row
html += `
| ${safeName} | `;
-
- // Days: Mo(1), Di(2), Mi(3), Do(4), Fr(5), Sa(6), So(0)
const dayOrder = [1, 2, 3, 4, 5, 6, 0];
-
for (const dayIdx of dayOrder) {
const dayDuties = data.byWeekday[dayIdx];
if (dayDuties.length === 0) {
@@ -990,16 +975,12 @@ class DienstplanApp {
let cellContent = '';
dayDuties.forEach(duty => {
const shareStr = duty.share === 0.5 ? '½' : '';
- // Determine tag style
- let tag = duty.isQual ? 'we-tag' : 'wt-tag';
-
- // Build cell content
+ const tag = duty.isQual ? 'we-tag' : 'wt-tag';
cellContent += `${shareStr}X
`;
});
html += `${cellContent} | `;
}
}
-
html += `
${bonus > 0 ? this.calculator.formatCurrency(bonus) : '-'} |
`;
@@ -1022,22 +1003,20 @@ class DienstplanApp {
html += `
-
Berechnungsregeln (Variante 2 - Streng):
+
Berechnungsregeln (NRW Psychiatrie 2011):
- - WE-Tage: Freitag, Samstag, Sonntag, Feiertage und Tage vor Feiertagen
- - Schwelle: Mindestens 2,0 WE-Einheiten für Bonuszahlung erforderlich
- - Vergütung bei Erreichen der Schwelle:
-
- - Werktage (WT): 250 € pro Einheit
- - WE-Tage: 450 € pro Einheit (abzüglich 2,0 Einheiten Abzug, Freitag zuerst)
-
-
- - Unter Schwelle: Keine Bonuszahlung (weder WT noch WE)
+ - Slots: Jeder Dienst wird in fr / sa / so / werktag klassifiziert. Tag vor Mo-Do-Feiertag = fr. Mo-Do-Feiertag = so. Sandwich-Tag (Feiertag + Tag-vor) = sa.
+ - V1: fr+so ≥ 1 UND werktag ≥ 3 → Abzug 1 (Fr-Prio) + 3 werktag.
+ - V2: sa ≥ 1 UND werktag ≥ 2 → Abzug 1 sa + 2 werktag.
+ - V3 (loose): fr+sa+so ≥ 2 → Abzug 2 aus Pool (Prio fr → so → sa).
+ - Auto-Select: Die Variante mit dem höchsten Bonus gewinnt; bei Gleichstand gewinnt die niedrigste Variantennummer.
+ - Urlaubsmodus (≥14 Tage frei): Halbiert alle Schwellen UND Abzüge.
+ - Sätze: Werktag = 250 EUR, Fr/Sa/So/Feiertag = 450 EUR.
- Erstellt am: ${new Date().toLocaleDateString('de-DE')} | Dienstplan NRW (Variante 2 - Streng)
+ Erstellt am: ${new Date().toLocaleDateString('de-DE')} | Dienstplan-Pro - NRW Psychiatrie 2011