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):

- Erstellt am: ${new Date().toLocaleDateString('de-DE')} | Dienstplan NRW (Variante 2 - Streng) + Erstellt am: ${new Date().toLocaleDateString('de-DE')} | Dienstplan-Pro - NRW Psychiatrie 2011