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