From fa64b7d91928c3df3d022b4ac1c38d087c5067c8 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Tue, 13 Jan 2026 16:27:27 -0500 Subject: [PATCH] feat(timesheet-approval): add overtime calculation for overview and timesheet details --- src/chatbot/chatbot.service.ts | 3 - .../pay-periods-build-overview.service.ts | 75 +++++++++++++------ .../timesheets/timesheet.mapper.ts | 25 ++++++- 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/chatbot/chatbot.service.ts b/src/chatbot/chatbot.service.ts index 6d3fbfb..5692bb8 100644 --- a/src/chatbot/chatbot.service.ts +++ b/src/chatbot/chatbot.service.ts @@ -3,7 +3,6 @@ import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { PageContextDto } from 'src/chatbot/dtos/page-context.dto'; -import { UserDto } from 'src/identity-and-account/users-management/user.dto'; import { Message } from 'src/chatbot/dtos/dialog-message.dto'; @Injectable() @@ -19,8 +18,6 @@ export class ChatbotService { ), ); - console.log('chatbot response: ', response); - const cleanText = Array.isArray(response.data) && response.data[0]?.output ? response.data[0].output diff --git a/src/time-and-attendance/pay-period/services/pay-periods-build-overview.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-build-overview.service.ts index 0878308..e0153e1 100644 --- a/src/time-and-attendance/pay-period/services/pay-periods-build-overview.service.ts +++ b/src/time-and-attendance/pay-period/services/pay-periods-build-overview.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@nestjs/common"; +import { Prisma } from "@prisma/client"; import { Result } from "src/common/errors/result-error.factory"; -import { computeHours, computePeriod, toDateFromString } from "src/common/utils/date-utils"; +import { computeHours, computePeriod, sevenDaysFrom, toDateFromString, toStringFromDate } from "src/common/utils/date-utils"; import { PrismaService } from "src/prisma/prisma.service"; import { EmployeePeriodOverviewDto, Overview, PayPeriodOverviewDto } from "src/time-and-attendance/pay-period/dtos/overview-pay-period.dto"; @@ -62,12 +63,15 @@ export class GetOverviewService { select: { id: true, is_approved: true, + start_date: true, shift: { select: { start_time: true, end_time: true, + date: true, bank_code: { select: { type: true } }, }, + orderBy: { date: 'asc' }, }, expense: { select: { @@ -91,8 +95,8 @@ export class GetOverviewService { } else { for (const employee of employee_overviews) { const record = this.createEmployeeSeeds( - employee.user.email, - employee.user.first_name, + employee.user.email, + employee.user.first_name, employee.user.last_name, employee.supervisor?.user ?? null, ); @@ -109,45 +113,68 @@ export class GetOverviewService { for (const employee of employee_overviews) { const record = ensure( - employee.id, - employee.user.first_name, - employee.user.last_name, + employee.id, + employee.user.first_name, + employee.user.last_name, employee.user.email ); for (const timesheet of employee.timesheet) { let total_weekly_hours: number = 0; + let daily_hours: number = 0; + let previous_shift_date: string = ''; //totals by types for shifts for (const shift of timesheet.shift) { const hours = computeHours(shift.start_time, shift.end_time); const type = (shift.bank_code?.type ?? '').toUpperCase(); + + if (previous_shift_date !== toStringFromDate(shift.date)) { + previous_shift_date = toStringFromDate(shift.date); + daily_hours = 0; + } + switch (type) { - case "EVENING": record.other_hours.evening_hours += hours; + case "EVENING": + if (total_weekly_hours + hours <= 40) { + record.other_hours.evening_hours += Math.min(hours, 8 - daily_hours); + record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0); + } else { + record.other_hours.evening_hours += Math.max(40 - total_weekly_hours, 0); + record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours); + } + total_weekly_hours += hours; + record.total_hours += hours; + break; + case "EMERGENCY": + record.other_hours.emergency_hours += hours; record.total_hours += hours; total_weekly_hours += hours; break; - case "EMERGENCY": record.other_hours.emergency_hours += hours; - record.total_hours += hours; - total_weekly_hours += hours; + case "SICK": + record.other_hours.sick_hours += hours; break; - case "OVERTIME": record.other_hours.overtime_hours += hours; - record.total_hours += hours; - total_weekly_hours += hours; - break; - case "SICK": record.other_hours.sick_hours += hours; - break; - case "HOLIDAY": record.other_hours.holiday_hours += hours; + case "HOLIDAY": + record.other_hours.holiday_hours += hours; record.total_hours += hours; total_weekly_hours += hours; break; case "VACATION": record.other_hours.vacation_hours += hours; break; - case "REGULAR": record.regular_hours += hours; - record.total_hours += hours; + case "REGULAR": + if (total_weekly_hours + hours <= 40) { + record.regular_hours += Math.min(hours, 8 - daily_hours); + record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0); + } else { + record.regular_hours += Math.max(40 - total_weekly_hours, 0); + record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours); + } total_weekly_hours += hours; + record.total_hours += hours; break; } + + daily_hours += hours; } //totals by type for expenses for (const expense of timesheet.expense) { @@ -170,7 +197,7 @@ export class GetOverviewService { if (!record) continue; const timesheets = employee.timesheet; const has_data = timesheets.some(timesheet => timesheet.shift.length > 0 || timesheet.expense.length > 0); - + const cutoff_date = new Date(); cutoff_date.setDate(cutoff_date.getDate() + 14); const is_active = employee.last_work_day ? employee.last_work_day.getTime() >= cutoff_date.getTime() : true; @@ -198,12 +225,12 @@ export class GetOverviewService { } createEmployeeSeeds = ( - email: string, - employee_first_name: string, - employee_last_name: string, + email: string, + employee_first_name: string, + employee_last_name: string, supervisor: { first_name: string; - last_name:string; + last_name: string; email: string; } | null = null, ): EmployeePeriodOverviewDto => ({ diff --git a/src/time-and-attendance/timesheets/timesheet.mapper.ts b/src/time-and-attendance/timesheets/timesheet.mapper.ts index f1e018d..4a04a8b 100644 --- a/src/time-and-attendance/timesheets/timesheet.mapper.ts +++ b/src/time-and-attendance/timesheets/timesheet.mapper.ts @@ -74,10 +74,28 @@ export const mapOneTimesheet = async (timesheet: Prisma.TimesheetsGetPayload<{ for (const shift of shifts_source) { const hours = diffOfHours(shift.start_time, shift.end_time); const subgroup = hoursSubGroupFromBankCode(shift.bank_code); - daily_hours[subgroup] += hours; - weekly_hours[subgroup] += hours; + const worked_weekly_hours = weekly_hours.regular + weekly_hours.emergency + weekly_hours.banking + weekly_hours.evening + weekly_hours.overtime + weekly_hours.holiday; + + if ((worked_weekly_hours + hours <= 40) && (subgroup === 'regular' || subgroup === 'evening')) { + daily_hours['overtime'] += Math.max(daily_hours[subgroup] + hours - 8, 0); + weekly_hours['overtime'] += Math.max(daily_hours[subgroup] + hours - 8, 0); + + weekly_hours[subgroup] += Math.min(hours, 8 - daily_hours[subgroup]); + daily_hours[subgroup] += Math.min(hours, 8 - daily_hours[subgroup]); + } else if (subgroup === 'regular' || subgroup === 'evening') { + daily_hours[subgroup] += Math.max((40 - worked_weekly_hours), 0); + daily_hours['overtime'] += Math.min((worked_weekly_hours + hours) - 40, hours); + + weekly_hours[subgroup] += Math.max((40 - worked_weekly_hours), 0); + weekly_hours['overtime'] += Math.min((worked_weekly_hours + hours) - 40, hours); + } else { + daily_hours[subgroup] += hours; + weekly_hours[subgroup] += hours; + } } + + //totals by expense types for (const expense of expenses_source) { const subgroup = expenseSubgroupFromBankCode(expense.bank_code); @@ -108,6 +126,9 @@ export const mapOneTimesheet = async (timesheet: Prisma.TimesheetsGetPayload<{ }; }); + weekly_hours['overtime'] = Math.max(weekly_hours['regular'] - 40, 0); + weekly_hours['regular'] = Math.min(weekly_hours['regular'], 40); + return { timesheet_id: timesheet.id, is_approved: timesheet.is_approved ?? false,