Dienstplan-Pro/calculator.js
Kenearos 722f961387 chore: cleanup dead code and duplicated logic from audit
- Remove broken webpack.yml (no package.json/webpack config exists)
- Drop dead RATE_NORMAL/RATE_WEEKEND fields on BonusCalculator; share the
  real rate constants across all three variants in variants.js
- Simplify getEmptyResult() to build allResults via .map instead of
  three manual spreads
- Replace 5 duplicated German month-name arrays and 2 weekday-name
  arrays in app.js with a shared monthName()/WEEKDAY_NAMES helper;
  drop the now-redundant getMonthName() method
- Reuse existing .modal/.modal-backdrop/.modal-content CSS classes in
  generateEmailReport() instead of hand-rolled inline styles
- Delegate ImageImporter.classify() to the shared classify() in
  variants.js instead of a duplicated copy
- Unify ad hoc YYYY-MM-DD formatting in app.js to use the existing
  HolidayProvider.formatDate() helper

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 19:23:14 +02:00

134 lines
4.5 KiB
JavaScript

/**
* Bonus Calculator (NRW Psychiatrie 2011)
* Orchestrator: classifies duties, runs all three variants (V1/V2/V3), picks the winner.
* Pure variant logic lives in variants.js.
*/
class BonusCalculator {
constructor(holidayProvider) {
this.holidayProvider = holidayProvider;
}
/**
* Whether the given date is a "qualifying" day (used by UI for badge coloring).
* Mirrors the old isQualifyingDay so app.js does not break.
*/
isQualifyingDay(date) {
const slot = classify(date, this.holidayProvider);
return slot !== 'weekday';
}
/**
* Human-readable label for the date's day type (used by UI).
*/
getDayTypeLabel(date) {
const dayOfWeek = date.getDay();
const isHoliday = this.holidayProvider.isHoliday(date);
const holidayName = this.holidayProvider.getHolidayName(date);
const isDayBefore = this.holidayProvider.isDayBeforeHoliday(date);
if (isHoliday) return `Feiertag (${holidayName})`;
if (isDayBefore) return 'Tag vor Feiertag';
if (dayOfWeek === 5) return 'Freitag';
if (dayOfWeek === 6) return 'Samstag';
if (dayOfWeek === 0) return 'Sonntag';
const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return days[dayOfWeek];
}
/**
* Build the dutyDetails array (date, share, isQualifying, dayType) for the UI.
*/
buildDutyDetails(duties) {
return duties.map(duty => ({
date: duty.date,
share: duty.share,
isQualifying: this.isQualifyingDay(duty.date),
dayType: this.getDayTypeLabel(duty.date)
}));
}
/**
* Calculate the bonus for a single employee for a given month.
* @param {Array} duties - Array of { date: Date, share: number }
* @param {boolean} isVacation - Vacation toggle (halves thresholds + deductions)
* @returns {Object} new-shape result (winner, allResults, totalBonus, classified, isVacation, dutyDetails)
*/
calculateMonthlyBonus(duties, isVacation = false) {
if (!duties || duties.length === 0) {
return this.getEmptyResult(isVacation);
}
const classified = classifyDuties(duties, this.holidayProvider);
const v1 = variant1(classified, isVacation);
const v2 = variant2(classified, isVacation);
const v3 = variant3(classified, isVacation);
const results = [v1, v2, v3];
// Pick winner: highest bonus; on tie prefer eligible over ineligible;
// further tie-break = lowest variantId (strict > preserves it).
let winner = results[0];
for (let i = 1; i < results.length; i++) {
const r = results[i];
if (r.bonus > winner.bonus) {
winner = r;
} else if (r.bonus === winner.bonus && r.eligible && !winner.eligible) {
winner = r;
}
}
winner.isWinner = true;
return {
classified,
isVacation,
winner,
allResults: results,
totalBonus: winner.bonus,
totalDuties: duties.length,
dutyDetails: this.buildDutyDetails(duties)
};
}
/**
* Calculate for all employees. vacationMap: { [employeeName]: boolean }
*/
calculateAllEmployees(employeeDuties, vacationMap = {}) {
const results = {};
for (const [name, duties] of Object.entries(employeeDuties)) {
const isVac = Boolean(vacationMap[name]);
results[name] = this.calculateMonthlyBonus(duties, isVac);
}
return results;
}
getEmptyResult(isVacation = false) {
const empty = {
variantId: 1,
eligible: false,
threshold: null,
deduction: { fr: 0, sa: 0, so: 0, weekday: 0 },
paidShares: { fr: 0, sa: 0, so: 0, weekday: 0 },
bonus: 0,
isWinner: false
};
return {
classified: { fr: 0, sa: 0, so: 0, weekday: 0 },
isVacation,
winner: { ...empty, isWinner: true },
allResults: [1, 2, 3].map(variantId => ({ ...empty, variantId, isWinner: variantId === 1 })),
totalBonus: 0,
totalDuties: 0,
dutyDetails: []
};
}
formatCurrency(amount) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(amount);
}
}
// Make it available globally
window.BonusCalculator = BonusCalculator;