194 lines
6.2 KiB
JavaScript
194 lines
6.2 KiB
JavaScript
/**
|
|
* Bonus-Varianten (NRW Psychiatrie 2011)
|
|
* Pure functions: day classification + V1/V2/V3 evaluation.
|
|
* Loaded after holidays.js and before calculator.js.
|
|
*/
|
|
|
|
// Will be implemented in subsequent tasks.
|
|
function classify(date, holidayProvider) {
|
|
const wd = date.getDay(); // 0=So, 1=Mo, ..., 5=Fr, 6=Sa
|
|
|
|
// Real Fr/Sa/So always win
|
|
if (wd === 5) return 'fr';
|
|
if (wd === 6) return 'sa';
|
|
if (wd === 0) return 'so';
|
|
|
|
// Mo-Do (wd 1..4)
|
|
const isFeiertag = holidayProvider.isHoliday(date);
|
|
const isTagVorFeiertag = holidayProvider.isDayBeforeHoliday(date);
|
|
|
|
if (isFeiertag && isTagVorFeiertag) return 'sa'; // Sandwich-Tag
|
|
if (isTagVorFeiertag) return 'fr'; // Tag vor Mo-Do-Feiertag
|
|
if (isFeiertag) return 'so'; // Feiertag Mo-Do
|
|
return 'weekday';
|
|
}
|
|
|
|
function classifyDuties(duties, holidayProvider) {
|
|
const result = { fr: 0, sa: 0, so: 0, weekday: 0 };
|
|
if (!Array.isArray(duties)) return result;
|
|
for (const duty of duties) {
|
|
const slot = classify(duty.date, holidayProvider);
|
|
result[slot] += duty.share;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function variant1(classified, isVacation) {
|
|
const RATE_NORMAL = 250;
|
|
const RATE_WEEKEND = 450;
|
|
const frSoThreshold = isVacation ? 0.5 : 1;
|
|
const weekdayThreshold = isVacation ? 1.5 : 3;
|
|
const frSoDeduction = isVacation ? 0.5 : 1;
|
|
const weekdayDeduction = isVacation ? 1.5 : 3;
|
|
|
|
const frSoPool = classified.fr + classified.so;
|
|
const eligible = (frSoPool >= frSoThreshold - 1e-9)
|
|
&& (classified.weekday >= weekdayThreshold - 1e-9);
|
|
|
|
if (!eligible) {
|
|
return {
|
|
variantId: 1,
|
|
eligible: false,
|
|
threshold: { frSo: frSoThreshold, weekday: weekdayThreshold },
|
|
deduction: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
paidShares: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
bonus: 0,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
// Friday priority within fr+so pool: fr first, then so
|
|
let remaining = frSoDeduction;
|
|
const deduction = { fr: 0, sa: 0, so: 0, weekday: weekdayDeduction };
|
|
for (const slot of ['fr', 'so']) {
|
|
const take = Math.min(remaining, classified[slot]);
|
|
deduction[slot] = take;
|
|
remaining -= take;
|
|
if (remaining <= 1e-9) break;
|
|
}
|
|
|
|
const paidShares = {
|
|
fr: Math.max(0, classified.fr - deduction.fr),
|
|
sa: classified.sa, // sa never deducted in V1
|
|
so: Math.max(0, classified.so - deduction.so),
|
|
weekday: Math.max(0, classified.weekday - deduction.weekday)
|
|
};
|
|
|
|
const bonus = (paidShares.fr + paidShares.sa + paidShares.so) * RATE_WEEKEND
|
|
+ paidShares.weekday * RATE_NORMAL;
|
|
|
|
return {
|
|
variantId: 1,
|
|
eligible: true,
|
|
threshold: { frSo: frSoThreshold, weekday: weekdayThreshold },
|
|
deduction,
|
|
paidShares,
|
|
bonus,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
function variant2(classified, isVacation) {
|
|
const RATE_NORMAL = 250;
|
|
const RATE_WEEKEND = 450;
|
|
const saThreshold = isVacation ? 0.5 : 1;
|
|
const weekdayThreshold = isVacation ? 1 : 2;
|
|
const saDeduction = isVacation ? 0.5 : 1;
|
|
const weekdayDeduction = isVacation ? 1 : 2;
|
|
|
|
const eligible = (classified.sa >= saThreshold - 1e-9)
|
|
&& (classified.weekday >= weekdayThreshold - 1e-9);
|
|
|
|
if (!eligible) {
|
|
return {
|
|
variantId: 2,
|
|
eligible: false,
|
|
threshold: { sa: saThreshold, weekday: weekdayThreshold },
|
|
deduction: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
paidShares: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
bonus: 0,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
const deduction = { fr: 0, sa: saDeduction, so: 0, weekday: weekdayDeduction };
|
|
|
|
const paidShares = {
|
|
fr: classified.fr, // fr never deducted in V2
|
|
sa: Math.max(0, classified.sa - deduction.sa),
|
|
so: classified.so, // so never deducted in V2
|
|
weekday: Math.max(0, classified.weekday - deduction.weekday)
|
|
};
|
|
|
|
const bonus = (paidShares.fr + paidShares.sa + paidShares.so) * RATE_WEEKEND
|
|
+ paidShares.weekday * RATE_NORMAL;
|
|
|
|
return {
|
|
variantId: 2,
|
|
eligible: true,
|
|
threshold: { sa: saThreshold, weekday: weekdayThreshold },
|
|
deduction,
|
|
paidShares,
|
|
bonus,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
function variant3(classified, isVacation) {
|
|
const RATE_NORMAL = 250;
|
|
const RATE_WEEKEND = 450;
|
|
const poolThreshold = isVacation ? 1 : 2;
|
|
const totalDeduction = isVacation ? 1 : 2;
|
|
|
|
const pool = classified.fr + classified.sa + classified.so;
|
|
const eligible = pool >= poolThreshold - 1e-9;
|
|
|
|
if (!eligible) {
|
|
return {
|
|
variantId: 3,
|
|
eligible: false,
|
|
threshold: { pool: poolThreshold },
|
|
deduction: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
paidShares: { fr: 0, sa: 0, so: 0, weekday: 0 },
|
|
bonus: 0,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
// Friday priority: fr -> so -> sa
|
|
let remaining = totalDeduction;
|
|
const deduction = { fr: 0, sa: 0, so: 0, weekday: 0 };
|
|
for (const slot of ['fr', 'so', 'sa']) {
|
|
const take = Math.min(remaining, classified[slot]);
|
|
deduction[slot] = take;
|
|
remaining -= take;
|
|
if (remaining <= 1e-9) break;
|
|
}
|
|
|
|
const paidShares = {
|
|
fr: Math.max(0, classified.fr - deduction.fr),
|
|
sa: Math.max(0, classified.sa - deduction.sa),
|
|
so: Math.max(0, classified.so - deduction.so),
|
|
weekday: classified.weekday // weekday never deducted in V3
|
|
};
|
|
|
|
const bonus = (paidShares.fr + paidShares.sa + paidShares.so) * RATE_WEEKEND
|
|
+ paidShares.weekday * RATE_NORMAL;
|
|
|
|
return {
|
|
variantId: 3,
|
|
eligible: true,
|
|
threshold: { pool: poolThreshold },
|
|
deduction,
|
|
paidShares,
|
|
bonus,
|
|
isWinner: false
|
|
};
|
|
}
|
|
|
|
// Expose globally
|
|
window.classify = classify;
|
|
window.classifyDuties = classifyDuties;
|
|
window.variant1 = variant1;
|
|
window.variant2 = variant2;
|
|
window.variant3 = variant3;
|