feat(timesheet-approval): add overtime calculation for overview and timesheet details

This commit is contained in:
Nicolas Drolet 2026-01-13 16:27:27 -05:00
parent 8f93c2b0f7
commit fa64b7d919
3 changed files with 74 additions and 29 deletions

View File

@ -3,7 +3,6 @@ import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { PageContextDto } from 'src/chatbot/dtos/page-context.dto'; 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'; import { Message } from 'src/chatbot/dtos/dialog-message.dto';
@Injectable() @Injectable()
@ -19,8 +18,6 @@ export class ChatbotService {
), ),
); );
console.log('chatbot response: ', response);
const cleanText = const cleanText =
Array.isArray(response.data) && response.data[0]?.output Array.isArray(response.data) && response.data[0]?.output
? response.data[0].output ? response.data[0].output

View File

@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { Result } from "src/common/errors/result-error.factory"; 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 { PrismaService } from "src/prisma/prisma.service";
import { EmployeePeriodOverviewDto, Overview, PayPeriodOverviewDto } from "src/time-and-attendance/pay-period/dtos/overview-pay-period.dto"; import { EmployeePeriodOverviewDto, Overview, PayPeriodOverviewDto } from "src/time-and-attendance/pay-period/dtos/overview-pay-period.dto";
@ -62,12 +63,15 @@ export class GetOverviewService {
select: { select: {
id: true, id: true,
is_approved: true, is_approved: true,
start_date: true,
shift: { shift: {
select: { select: {
start_time: true, start_time: true,
end_time: true, end_time: true,
date: true,
bank_code: { select: { type: true } }, bank_code: { select: { type: true } },
}, },
orderBy: { date: 'asc' },
}, },
expense: { expense: {
select: { select: {
@ -91,8 +95,8 @@ export class GetOverviewService {
} else { } else {
for (const employee of employee_overviews) { for (const employee of employee_overviews) {
const record = this.createEmployeeSeeds( const record = this.createEmployeeSeeds(
employee.user.email, employee.user.email,
employee.user.first_name, employee.user.first_name,
employee.user.last_name, employee.user.last_name,
employee.supervisor?.user ?? null, employee.supervisor?.user ?? null,
); );
@ -109,45 +113,68 @@ export class GetOverviewService {
for (const employee of employee_overviews) { for (const employee of employee_overviews) {
const record = ensure( const record = ensure(
employee.id, employee.id,
employee.user.first_name, employee.user.first_name,
employee.user.last_name, employee.user.last_name,
employee.user.email employee.user.email
); );
for (const timesheet of employee.timesheet) { for (const timesheet of employee.timesheet) {
let total_weekly_hours: number = 0; let total_weekly_hours: number = 0;
let daily_hours: number = 0;
let previous_shift_date: string = '';
//totals by types for shifts //totals by types for shifts
for (const shift of timesheet.shift) { for (const shift of timesheet.shift) {
const hours = computeHours(shift.start_time, shift.end_time); const hours = computeHours(shift.start_time, shift.end_time);
const type = (shift.bank_code?.type ?? '').toUpperCase(); 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) { 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; record.total_hours += hours;
total_weekly_hours += hours; total_weekly_hours += hours;
break; break;
case "EMERGENCY": record.other_hours.emergency_hours += hours; case "SICK":
record.total_hours += hours; record.other_hours.sick_hours += hours;
total_weekly_hours += hours;
break; break;
case "OVERTIME": record.other_hours.overtime_hours += hours; case "HOLIDAY":
record.total_hours += hours; record.other_hours.holiday_hours += hours;
total_weekly_hours += hours;
break;
case "SICK": record.other_hours.sick_hours += hours;
break;
case "HOLIDAY": record.other_hours.holiday_hours += hours;
record.total_hours += hours; record.total_hours += hours;
total_weekly_hours += hours; total_weekly_hours += hours;
break; break;
case "VACATION": record.other_hours.vacation_hours += hours; case "VACATION": record.other_hours.vacation_hours += hours;
break; break;
case "REGULAR": record.regular_hours += hours; case "REGULAR":
record.total_hours += hours; 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; total_weekly_hours += hours;
record.total_hours += hours;
break; break;
} }
daily_hours += hours;
} }
//totals by type for expenses //totals by type for expenses
for (const expense of timesheet.expense) { for (const expense of timesheet.expense) {
@ -170,7 +197,7 @@ export class GetOverviewService {
if (!record) continue; if (!record) continue;
const timesheets = employee.timesheet; const timesheets = employee.timesheet;
const has_data = timesheets.some(timesheet => timesheet.shift.length > 0 || timesheet.expense.length > 0); const has_data = timesheets.some(timesheet => timesheet.shift.length > 0 || timesheet.expense.length > 0);
const cutoff_date = new Date(); const cutoff_date = new Date();
cutoff_date.setDate(cutoff_date.getDate() + 14); cutoff_date.setDate(cutoff_date.getDate() + 14);
const is_active = employee.last_work_day ? employee.last_work_day.getTime() >= cutoff_date.getTime() : true; const is_active = employee.last_work_day ? employee.last_work_day.getTime() >= cutoff_date.getTime() : true;
@ -198,12 +225,12 @@ export class GetOverviewService {
} }
createEmployeeSeeds = ( createEmployeeSeeds = (
email: string, email: string,
employee_first_name: string, employee_first_name: string,
employee_last_name: string, employee_last_name: string,
supervisor: { supervisor: {
first_name: string; first_name: string;
last_name:string; last_name: string;
email: string; email: string;
} | null = null, } | null = null,
): EmployeePeriodOverviewDto => ({ ): EmployeePeriodOverviewDto => ({

View File

@ -74,10 +74,28 @@ export const mapOneTimesheet = async (timesheet: Prisma.TimesheetsGetPayload<{
for (const shift of shifts_source) { for (const shift of shifts_source) {
const hours = diffOfHours(shift.start_time, shift.end_time); const hours = diffOfHours(shift.start_time, shift.end_time);
const subgroup = hoursSubGroupFromBankCode(shift.bank_code); const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
daily_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;
weekly_hours[subgroup] += hours;
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 //totals by expense types
for (const expense of expenses_source) { for (const expense of expenses_source) {
const subgroup = expenseSubgroupFromBankCode(expense.bank_code); 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 { return {
timesheet_id: timesheet.id, timesheet_id: timesheet.id,
is_approved: timesheet.is_approved ?? false, is_approved: timesheet.is_approved ?? false,