153 lines
6.3 KiB
TypeScript
153 lines
6.3 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { PrismaService } from '../../../prisma/prisma.service';
|
|
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
@Injectable()
|
|
export class OvertimeService {
|
|
|
|
private logger = new Logger(OvertimeService.name);
|
|
private daily_max = 8; // maximum for regular hours per day
|
|
private weekly_max = 40; //maximum for regular hours per week
|
|
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation
|
|
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
//calculate daily overtime
|
|
async getDailyOvertimeHoursForDay(employee_id: number, date: Date): Promise<number> {
|
|
const shifts = await this.prisma.shifts.findMany({
|
|
where: { date: date, timesheet: { employee_id: employee_id } },
|
|
select: { start_time: true, end_time: true },
|
|
});
|
|
const total = shifts.map((shift)=>
|
|
computeHours(shift.start_time, shift.end_time, 5)).reduce((sum, hours)=> sum + hours, 0);
|
|
const overtime = Math.max(0, total - this.daily_max);
|
|
|
|
this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`);
|
|
return overtime;
|
|
}
|
|
|
|
//calculate Weekly overtime
|
|
async getWeeklyOvertimeHours(employee_id: number, ref_date: Date): Promise<number> {
|
|
const week_start = getWeekStart(ref_date);
|
|
const week_end = getWeekEnd(week_start);
|
|
|
|
//fetches all shifts from INCLUDED_TYPES array
|
|
const included_shifts = await this.prisma.shifts.findMany({
|
|
where: {
|
|
date: { gte:week_start, lte: week_end },
|
|
timesheet: { employee_id },
|
|
bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } },
|
|
},
|
|
select: { start_time: true, end_time: true },
|
|
orderBy: [{date: 'asc'}, {start_time:'asc'}],
|
|
});
|
|
|
|
//calculate total hours of those shifts minus weekly Max to find total overtime hours
|
|
const total = included_shifts.map(shift => computeHours(shift.start_time, shift.end_time, 5))
|
|
.reduce((sum, hours)=> sum+hours, 0);
|
|
const overtime = Math.max(0, total - this.weekly_max);
|
|
|
|
this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`);
|
|
return overtime;
|
|
}
|
|
|
|
//transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift
|
|
async transformRegularHoursToWeeklyOvertime(
|
|
employee_id: number,
|
|
ref_date: Date,
|
|
tx?: Prisma.TransactionClient,
|
|
): Promise<void> {
|
|
//ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected.
|
|
const db = tx ?? this.prisma;
|
|
|
|
//calculate weekly overtime
|
|
const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date);
|
|
if(overtime_hours <= 0) return;
|
|
|
|
const convert_to_minutes = Math.round(overtime_hours * 60);
|
|
|
|
const [regular, overtime] = await Promise.all([
|
|
db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }),
|
|
db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }),
|
|
]);
|
|
if(!regular || !overtime) return;
|
|
|
|
const week_start = getWeekStart(ref_date);
|
|
const week_end = getWeekEnd(week_start);
|
|
|
|
//gets all regular shifts and order them by desc
|
|
const regular_shifts_desc = await db.shifts.findMany({
|
|
where: {
|
|
date: { gte:week_start, lte: week_end },
|
|
timesheet: { employee_id },
|
|
bank_code_id: regular.id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
timesheet_id: true,
|
|
date: true,
|
|
start_time: true,
|
|
end_time: true,
|
|
is_remote: true,
|
|
comment: true,
|
|
},
|
|
orderBy: [{date: 'desc'}, {start_time:'desc'}],
|
|
});
|
|
|
|
let remaining_minutes = convert_to_minutes;
|
|
|
|
for(const shift of regular_shifts_desc) {
|
|
if(remaining_minutes <= 0) break;
|
|
|
|
const start = shift.start_time;
|
|
const end = shift.end_time;
|
|
const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000));
|
|
if(duration_in_minutes === 0) continue;
|
|
|
|
if(duration_in_minutes <= remaining_minutes) {
|
|
await db.shifts.update({
|
|
where: { id: shift.id },
|
|
data: { bank_code_id: overtime.id },
|
|
});
|
|
remaining_minutes -= duration_in_minutes;
|
|
continue;
|
|
}
|
|
//sets the start_time of the new overtime shift
|
|
const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000);
|
|
|
|
//shorten the regular shift
|
|
await db.shifts.update({
|
|
where: { id: shift.id },
|
|
data: { end_time: new_overtime_start },
|
|
});
|
|
|
|
//creates the new overtime shift to replace the shorten regular shift
|
|
await db.shifts.create({
|
|
data: {
|
|
timesheet_id: shift.timesheet_id,
|
|
date: shift.date,
|
|
start_time: new_overtime_start,
|
|
end_time: end,
|
|
is_remote: shift.is_remote,
|
|
comment: shift.comment,
|
|
bank_code_id: overtime.id,
|
|
},
|
|
});
|
|
remaining_minutes = 0;
|
|
}
|
|
this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id}
|
|
week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)}
|
|
converted= ${(convert_to_minutes-remaining_minutes)/60}h`);
|
|
}
|
|
|
|
//apply modifier to overtime hours
|
|
// calculateOvertimePay(overtime_hours: number, modifier: number): number {
|
|
// const pay = overtime_hours * modifier;
|
|
// this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtime_hours}, modifier ${modifier})`);
|
|
|
|
// return pay;
|
|
// }
|
|
|
|
}
|