This repository has been archived on 2026-06-28. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Dienstplan/webapp/calculator.js
Claude 520e3b62e0
Add web-based duty schedule bonus calculator
Implements a complete web application for calculating bonus payments
for weekend and holiday duty shifts according to NRW rules.

Features:
- Employee management (add/remove multiple employees)
- Monthly duty scheduling (full and half shifts)
- Automatic NRW holiday detection (2025-2030)
- Bonus calculation with configurable rules
- LocalStorage for data persistence
- Export/Import functionality (JSON)
- Responsive design for desktop and mobile
- No external dependencies

Calculation Rules:
- Qualifying days: Friday, Saturday, Sunday, public holidays, day before holiday
- Minimum threshold: 2.0 qualifying days required
- Deduction: 1.0 qualifying day after threshold reached
- Rates: Normal days 250€, qualifying days 450€
- Half shifts: 50% of respective rate
- No bonus payment if threshold not reached

Technical Stack:
- Vanilla JavaScript (no frameworks)
- HTML5 & CSS3
- LocalStorage API
- Modern, gradient-based UI design

Files:
- webapp/index.html - Main HTML interface
- webapp/styles.css - Responsive styling
- webapp/app.js - Main application logic and UI handling
- webapp/calculator.js - Bonus calculation engine
- webapp/holidays.js - NRW public holidays provider
- webapp/storage.js - LocalStorage data management
- webapp/README.md - Comprehensive documentation

Updated main README.md to include web app in available versions.
2025-11-18 20:00:13 +00:00

176 lines
5.5 KiB
JavaScript

/**
* Duty Schedule Bonus Calculator
* Calculates bonuses based on weekend and holiday duty shifts
*/
class BonusCalculator {
constructor(holidayProvider) {
this.holidayProvider = holidayProvider;
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
}
/**
* Check if a date is a qualifying day (weekend or holiday related)
* Qualifying days: Friday, Saturday, Sunday, Public Holiday, Day before public holiday
* @param {Date} date
* @returns {boolean}
*/
isQualifyingDay(date) {
const dayOfWeek = date.getDay(); // 0 = Sunday, 5 = Friday, 6 = Saturday
// Weekend: Friday (5), Saturday (6), Sunday (0)
const isWeekend = dayOfWeek === 5 || dayOfWeek === 6 || dayOfWeek === 0;
// Public holiday
const isHoliday = this.holidayProvider.isHoliday(date);
// Day before public holiday
const isDayBeforeHoliday = this.holidayProvider.isDayBeforeHoliday(date);
return isWeekend || isHoliday || isDayBeforeHoliday;
}
/**
* Get day type label for display
* @param {Date} date
* @returns {string}
*/
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];
}
/**
* Calculate bonus for a single employee for a given month
* @param {Array} duties - Array of duty objects: {date: Date, share: number (1.0 or 0.5)}
* @returns {Object} Calculation result
*/
calculateMonthlyBonus(duties) {
if (!duties || duties.length === 0) {
return this.getEmptyResult();
}
// Separate qualifying and non-qualifying days
let qualifyingDays = 0;
let normalDays = 0;
const dutyDetails = [];
duties.forEach(duty => {
const isQualifying = this.isQualifyingDay(duty.date);
const dayType = this.getDayTypeLabel(duty.date);
if (isQualifying) {
qualifyingDays += duty.share;
} else {
normalDays += duty.share;
}
dutyDetails.push({
date: duty.date,
share: duty.share,
isQualifying: isQualifying,
dayType: dayType
});
});
// Check if threshold is reached
const thresholdReached = qualifyingDays >= this.MIN_QUALIFYING_DAYS;
let bonus = 0;
let normalDaysPaid = 0;
let qualifyingDaysPaid = 0;
let qualifyingDaysDeducted = 0;
if (thresholdReached) {
// Deduct 1.0 qualifying day
qualifyingDaysDeducted = 1.0;
qualifyingDaysPaid = Math.max(0, qualifyingDays - qualifyingDaysDeducted);
normalDaysPaid = normalDays;
// Calculate bonus
bonus = (normalDaysPaid * this.RATE_NORMAL) + (qualifyingDaysPaid * this.RATE_WEEKEND);
}
return {
totalDuties: duties.length,
totalDaysWorked: qualifyingDays + normalDays,
normalDays: normalDays,
qualifyingDays: qualifyingDays,
thresholdReached: thresholdReached,
qualifyingDaysDeducted: qualifyingDaysDeducted,
normalDaysPaid: normalDaysPaid,
qualifyingDaysPaid: qualifyingDaysPaid,
bonusNormalDays: normalDaysPaid * this.RATE_NORMAL,
bonusQualifyingDays: qualifyingDaysPaid * this.RATE_WEEKEND,
totalBonus: bonus,
dutyDetails: dutyDetails
};
}
/**
* Calculate bonuses for all employees
* @param {Object} employeeDuties - Object with employee names as keys and duty arrays as values
* @returns {Object} Results for all employees
*/
calculateAllEmployees(employeeDuties) {
const results = {};
for (const [employeeName, duties] of Object.entries(employeeDuties)) {
results[employeeName] = this.calculateMonthlyBonus(duties);
}
return results;
}
/**
* Get empty result structure
* @returns {Object}
*/
getEmptyResult() {
return {
totalDuties: 0,
totalDaysWorked: 0,
normalDays: 0,
qualifyingDays: 0,
thresholdReached: false,
qualifyingDaysDeducted: 0,
normalDaysPaid: 0,
qualifyingDaysPaid: 0,
bonusNormalDays: 0,
bonusQualifyingDays: 0,
totalBonus: 0,
dutyDetails: []
};
}
/**
* Format currency for display
* @param {number} amount
* @returns {string}
*/
formatCurrency(amount) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(amount);
}
}
// Make it available globally
window.BonusCalculator = BonusCalculator;