targo-backend/src/modules/business-logics/services/overtime.service.ts
Matthieu Haineault 4527b0e7f7 fix(shifts): clean
2025-10-09 15:22:34 -04:00

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;
// }
}