section-2
Version:
A library for calculating unsocial hours entitlements under the NHS agenda for change's section 2
262 lines (261 loc) • 13.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateAdditionalHours = exports.calculateCumulativeAdditionalHours = void 0;
const conversions_1 = require("../utils/conversions");
const shiftTypes_1 = require("../utils/shiftTypes");
const shiftLengths_1 = require("../utils/shiftLengths");
const validateTimestamp_1 = require("../utils/validateTimestamp");
const calculateCumulativeAdditionalHours = (from, shifts = [], id, employment_id) => {
const prevMonday = new Date(from);
prevMonday.setHours(0, 0, 0, 0);
prevMonday.setDate(prevMonday.getDate() - ((prevMonday.getDay() + 6) % 7));
return shifts.reduce((acc, val) => {
const valFrom = new Date(val.from);
const valTo = (0, conversions_1.makeToAlwaysLater)(valFrom, val.actual_to).toObj;
if (valFrom >= prevMonday &&
valFrom < from &&
((!!id && !!val.id && id !== val.id) || (!id && !val.id)) &&
((!!employment_id &&
!!val.employment_id &&
employment_id === val.employment_id) ||
(!employment_id && !val.employment_id))) {
if (shiftTypes_1.additionalHoursShiftTypes.includes(val.type)) {
return (acc +
(0, shiftLengths_1.calculateShiftHours)(valFrom, valTo, val.break_override));
}
else if (val.overrun_type === "OT") {
return (acc +
(0, shiftLengths_1.calculateShiftHours)((0, conversions_1.makeToAlwaysLater)(valFrom, val.planned_to).toObj, valTo, val.break_override));
}
}
return acc;
}, 0);
};
exports.calculateCumulativeAdditionalHours = calculateCumulativeAdditionalHours;
const calculateRawBankHoliday = (from, to) => {
// calculate in ms the total time within the bank holiday windows
if (!from || !to) {
return 0;
}
const fromObj = new Date(from);
const toObj = new Date(to);
const fromTime = fromObj.getTime();
const toTime = toObj.getTime();
if (!(0, shiftLengths_1.isBankHoliday)(fromObj) && !(0, shiftLengths_1.isBankHoliday)(toObj)) {
// neither BH
return 0;
}
if ((0, shiftLengths_1.isBankHoliday)(fromObj) && (0, shiftLengths_1.isBankHoliday)(toObj)) {
// all BH
return toTime - fromTime;
}
const to0000 = toObj.setHours(0, 0, 0, 0);
if (!(0, shiftLengths_1.isBankHoliday)(fromObj) && (0, shiftLengths_1.isBankHoliday)(toObj)) {
// non-BH into BH
return toTime - to0000;
}
if ((0, shiftLengths_1.isBankHoliday)(fromObj) && !(0, shiftLengths_1.isBankHoliday)(toObj)) {
// BH into non-BH
return to0000 - fromTime;
}
return 0;
};
const calculateBankHolidayHours = (from, planned_to, actual_to, fromIsBankHoliday, toIsBankHoliday, break_override = null, type = "OT", overrun_type = "OT", hoursUntilThresholdMs = 0, break_from_higher = true) => {
let plannedDouble = 0;
// plannedToil is used to calculate proportions for where breaks should be deducted, but only planned shifts attract it
let plannedToil = 0;
let overrunDouble = 0;
if ((type === "Normal" ||
type === "OT" ||
(type === "TOIL" && overrun_type === "OT")) &&
(fromIsBankHoliday || toIsBankHoliday)) {
const fromObj = new Date(from);
const plannedToObj = new Date(planned_to);
const actualToObj = new Date(actual_to);
const plannedLength = plannedToObj.getTime() - fromObj.getTime();
const breakLength = (0, shiftLengths_1.calculateBreak)(plannedLength, break_override);
const flatTo = new Date(Math.min(actualToObj.getTime(), (type === "Normal" || type === "TOIL"
? plannedToObj.getTime()
: fromObj.getTime()) +
hoursUntilThresholdMs +
(type === "TOIL" ? 0 : breakLength)));
//planned hours
if (type === "Normal" ||
hoursUntilThresholdMs >= plannedLength - breakLength) {
//normal shift - just add earned TOIL
plannedToil += calculateRawBankHoliday(fromObj, plannedToObj);
}
else if (type === "OT" && hoursUntilThresholdMs <= 0) {
//shift is all OT hours - just add earned double
plannedDouble += calculateRawBankHoliday(fromObj, plannedToObj);
}
else {
if (flatTo > fromObj) {
//mixed flat and OT shift - add TOIL for flat hours before threshold (including break)
plannedToil += Math.max(0, calculateRawBankHoliday(fromObj, new Date(Math.min(plannedToObj.getTime(), flatTo.getTime()))));
}
if (flatTo < plannedToObj) {
//add any planned hours over threshold to double if still in BH window (excluding break)
plannedDouble += calculateRawBankHoliday(flatTo, plannedToObj);
}
}
//breaks
if (breakLength) {
if (type === "Normal" ||
hoursUntilThresholdMs >= plannedLength - breakLength) {
if (break_from_higher || plannedLength === plannedToil) {
//shift is all toil, or break comes from higher rate first which aligns with toil
plannedToil -= Math.min(breakLength, Math.max(0, plannedToil - breakLength));
}
}
else if (hoursUntilThresholdMs <= 0) {
//shift is all OT hours
if (plannedDouble >= plannedLength) {
//shift is all double
plannedDouble -= Math.min(breakLength, Math.max(0, plannedDouble - breakLength));
}
else {
const timeAndHalf = Math.max(0, plannedLength - hoursUntilThresholdMs - plannedDouble);
let doubleBreak = 0;
if (break_from_higher) {
doubleBreak = Math.min(breakLength, plannedDouble);
}
else {
// take break from less expensive side first (time_and_half is calculated elsewhere)
const timeAndHalfBreak = Math.min(timeAndHalf, breakLength);
doubleBreak = Math.max(0, breakLength - timeAndHalfBreak);
}
plannedDouble = Math.max(0, plannedDouble - doubleBreak);
}
}
else {
//shift is a mix of TOIL and OT
//calculate break in TOIL section
const nonBhBreak = Math.min(breakLength, Math.max(0, plannedLength - plannedToil - plannedDouble));
const toilBreak = break_from_higher
? Math.min(breakLength, plannedToil)
: Math.min(Math.max(0, breakLength - nonBhBreak), plannedToil);
plannedToil -= toilBreak;
if (toilBreak < breakLength) {
const timeAndHalf = Math.max(0, plannedLength - hoursUntilThresholdMs - plannedDouble);
let doubleBreak = 0;
if (break_from_higher) {
doubleBreak = Math.min(breakLength, plannedDouble);
}
else {
// take break from less expensive side first (time_and_half is calculated elsewhere)
const timeAndHalfBreak = Math.min(timeAndHalf, breakLength);
doubleBreak = Math.max(0, breakLength - timeAndHalfBreak);
}
plannedDouble -= doubleBreak;
}
}
}
//overrun hours
if (overrun_type === "OT" && actualToObj > plannedToObj) {
const otFrom = type === "Normal"
? new Date(Math.min(plannedToObj.getTime() + hoursUntilThresholdMs, actualToObj.getTime()))
: flatTo;
if (otFrom < actualToObj) {
//add OT double
overrunDouble = calculateRawBankHoliday(otFrom < plannedToObj ? plannedToObj : otFrom, actualToObj);
}
}
}
return {
// only normal shifts can earn toil
toil: type === "Normal" ? plannedToil : 0,
double: plannedDouble + overrunDouble,
};
};
/**
* Calculates Additional hours object containing calculated additional in ms
*
* @param shift - shift parameters for calculating
* @param shift.id - the unique identifier of the shift, optional
* @param shift.from - the Date object representing the start of the shift
* @param shift.planned_to - the Date object representing of the planned end of the shift
* @param shift.actual_to - the Date object representing of the actual end of the shift
* @param shift.employment_id - the unique identifier of the shift's employment, defaults to `undefined`
* @param shift.type - the type of shift, defaults to `"Normal"`
* @param shift.overrun_type - the type of overrun, defaults to `"OT"`
* @param shift.break_override - the duration of the shift's break in ms to override the default of 30mins every 6hrs, defaults to `null`
* @param shift.weekly_hours - the weekly hours contracted in ms
* @param shifts - an array of shifts, used to calculate cumulative additional hours for crossing the 37.5hrs/wk threshold
*
* @returns AdditionalHours
* @returns 0 for all fields if invalid Dates are passed
*/
const calculateAdditionalHours = ({ id, from, planned_to, actual_to, employment_id, type, overrun_type, weekly_hours, break_override = null, break_from_higher = true, }, shifts) => {
const additionalHoursBreakdown = {
flat: 0,
time_and_half: 0,
double: 0,
toil: 0,
absent_hours: 0,
};
if (!(0, validateTimestamp_1.validateTimestampParameters)(from, planned_to, actual_to)) {
//times are either invalid or out of sequence - return 0 for everything
return additionalHoursBreakdown;
}
if (type && from && actual_to && planned_to) {
const isAdditionalHoursShift = shiftTypes_1.additionalHoursShiftTypes.includes(type);
const fromObj = new Date(from);
const plannedToObj = new Date(planned_to);
const actualToObj = new Date(actual_to);
const fromIsBankHoliday = (0, shiftLengths_1.isBankHoliday)(fromObj);
const toIsBankHoliday = (0, shiftLengths_1.isBankHoliday)(actualToObj);
const cumulativeAdditionalHours = type === "Bank"
? 0
: (0, exports.calculateCumulativeAdditionalHours)(fromObj, shifts, id, employment_id);
const plannedAdditionalHours = isAdditionalHoursShift
? (0, shiftLengths_1.calculateShiftHours)(fromObj, planned_to, break_override)
: 0;
const overrunHours = plannedToObj === actualToObj
? 0
: (0, shiftLengths_1.calculateShiftLength)(plannedToObj, actualToObj);
let paidAdditionalHours = plannedAdditionalHours +
(overrun_type === "OT" || type === "Bank" ? overrunHours : 0);
if (shiftTypes_1.absentShiftTypes.includes(type)) {
additionalHoursBreakdown.absent_hours = (0, shiftLengths_1.calculateShiftHours)(fromObj, plannedToObj, break_override);
}
else if (paidAdditionalHours) {
const weeklyOtThresholdHours = 1.35e8 - (weekly_hours ?? 1.35e8); //37.5hrs fallback but allowing for 0
const { toil: bhToil, double: bhDouble } = calculateBankHolidayHours(new Date(from), new Date(planned_to), new Date(actual_to), fromIsBankHoliday, toIsBankHoliday, break_override, type, overrun_type, Math.max(0, weeklyOtThresholdHours - cumulativeAdditionalHours), break_from_higher);
additionalHoursBreakdown.toil += bhToil;
if (type === "Normal" || type === "OT" || type === "TOIL") {
if (type === "TOIL") {
additionalHoursBreakdown.toil += plannedAdditionalHours;
paidAdditionalHours = Math.max(0, paidAdditionalHours - plannedAdditionalHours);
}
additionalHoursBreakdown.flat = Math.max(0, Math.min(paidAdditionalHours, weeklyOtThresholdHours - cumulativeAdditionalHours));
if (paidAdditionalHours + cumulativeAdditionalHours >=
weeklyOtThresholdHours) {
if (fromIsBankHoliday || toIsBankHoliday) {
if (type === "OT" || overrun_type === "OT") {
additionalHoursBreakdown.double += bhDouble;
}
}
additionalHoursBreakdown.time_and_half = Math.max(0, paidAdditionalHours -
additionalHoursBreakdown.double -
additionalHoursBreakdown.flat);
}
}
else if (type === "Bank") {
additionalHoursBreakdown.flat = Math.floor(paidAdditionalHours * 1.12004801920768);
}
}
else if (type === "Normal") {
additionalHoursBreakdown.toil += calculateBankHolidayHours(new Date(from), new Date(planned_to), new Date(actual_to), fromIsBankHoliday, toIsBankHoliday, break_override, type, overrun_type, Math.max(0, 1.35e8 -
(weekly_hours ?? 1.35e8) -
cumulativeAdditionalHours), break_from_higher).toil;
}
if (["Normal", ...shiftTypes_1.additionalHoursShiftTypes].includes(type) &&
overrun_type === "TOIL" &&
type !== "Bank") {
additionalHoursBreakdown.toil += overrunHours;
}
}
return additionalHoursBreakdown;
};
exports.calculateAdditionalHours = calculateAdditionalHours;