feat(utils): added date-utils.ts and refactor services to use it

This commit is contained in:
Matthieu Haineault 2025-08-05 11:39:34 -04:00
parent 9914e07ff3
commit 50c3bca11b
8 changed files with 74 additions and 70 deletions

View File

@ -0,0 +1,50 @@
//lenght of a shift, rouded to nearest 'x' minute
export function computeHours(start: Date, end: Date, roundToMinutes?: number): number {
const diffMs = end.getTime() - start.getTime();
const totalMinutes = diffMs / 60000;
const minutes = roundToMinutes ?
Math.round(totalMinutes / roundToMinutes) * roundToMinutes :
totalMinutes;
return +(minutes / 60).toFixed(2);
}
//round the amount of hours to quarter
export function roundToQuarterHour(hours: number): number {
return Math.round(hours *4) / 4;
}
//calculate the number of the week (1 or 2)
export function computeWeekNumber(periodStart: Date, targetDate: Date): number {
const days = Math.floor( targetDate.getTime() - periodStart.getTime()) /
(1000 * 60 * 60 * 24);
return Math.floor(days / 7) +1;
}
//Date format YYY-MM-DD
export function formatDateISO(d:Date): string {
return d.toISOString().split('T')[0];
}
//fetch firts day of the week (Sunday)
export function getWeekStart(date:Date, firstDayOfWeek = 0): Date {
const d = new Date(date);
const day = d.getDay();
const diff = (day < firstDayOfWeek ? 7 : 0) + (day - firstDayOfWeek);
d.setDate(d.getDate() - diff);
d.setHours(0,0,0,0);
return d;
}
//fetch last day of the week (Saturday)
export function getWeekEnd(startOfWeek: Date): Date {
const d = new Date(startOfWeek);
d.setDate(d.getDate() + 6);
d.setHours(23,59,59,999);
return d;
}
//returns january 1st of the selected date's year
export function getYearStart(date:Date): Date {
return new Date(date.getFullYear(),0,1,0,0,0,0);
}

View File

@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
//THIS SERVICE IS NOT USED RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING
//THIS SERVICE IS NOT USED, RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING
@Injectable()
export class AfterHoursService {
private readonly logger = new Logger(AfterHoursService.name);

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
@Injectable()
export class HolidayService {
@ -7,32 +8,15 @@ export class HolidayService {
constructor(private readonly prisma: PrismaService) {}
//return the sunday of the current week that includes the holiday
private getWeekStart(date: Date): Date {
const day = new Date(date);
const offset = day.getDay();
day.setDate(day.getDate() - offset);
day.setHours(0,0,0,0);
return day;
}
//rounds minutes to 5s
private computeHours(start: Date, end: Date): number {
const durationMS = end.getTime() - start.getTime();
const totalMinutes = durationMS / 60000;
const rounded = Math.round(totalMinutes / 5) * 5;
return rounded / 60;
}
private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> {
//sets the end of the window to 1ms before the week with the holiday
const holidayWeekStart = this.getWeekStart(holidayDate);
const holidayWeekStart = getWeekStart(holidayDate);
const windowEnd = new Date(holidayWeekStart.getTime() - 1);
//sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday
const windowStart = new Date(windowEnd.getTime() - 28 * 24 * 60 * 60000 + 1 )
const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700'];
//fetches all shift of the employee in said window ( 4 completed weeks )
//fetches all shift of the employee in said window ( 4 previous completed weeks )
const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId } ,
date: { gte: windowStart, lte: windowEnd },
@ -41,7 +25,7 @@ export class HolidayService {
select: { date: true, start_time: true, end_time: true },
});
const totalHours = shifts.map(s => this.computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
const totalHours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
const dailyHours = totalHours / 20;
return dailyHours;

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
@Injectable()
export class OvertimeService {
@ -10,47 +11,18 @@ export class OvertimeService {
constructor(private prisma: PrismaService) {}
// calculate decimal hours rounded to nearest 5 min
computedHours(start: Date, end: Date): number {
const durationMs = end.getTime() - start.getTime();
const totalMinutes = durationMs / 60000;
//rounded to 5 min
const rounded = Math.round(totalMinutes / 5) * 5;
const hours = rounded / 60;
this.logger.debug(`computedHours: raw=${totalMinutes.toFixed(1)}min rounded = ${rounded}min (${hours.toFixed(2)}h)`);
return hours;
}
//calculate Daily overtime
getDailyOvertimeHours(start: Date, end: Date): number {
const hours = this.computedHours(start, end);
const hours = computeHours(start, end, 5);
const overtime = Math.max(0, hours - this.dailyMax);
this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`);
return overtime;
}
//sets first day of the week to be sunday
private getWeekStart(date:Date): Date {
const d = new Date(date);
const day = d.getDay(); // return sunday = 0, monday = 1, etc
d.setDate(d.getDate() - day);
d.setHours(0,0,0,0,); // puts start of the week at sunday morning at 00:00
return d;
}
//sets last day of the week to be saturday
private getWeekEnd(startDate:Date): Date {
const d = new Date(startDate);
d.setDate(d.getDate() +6); //sets last day to be saturday
d.setHours(23,59,59,999); //puts end of the week at saturday night at 00:00 minus 1ms
return d;
}
//calculate Weekly overtime
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
const weekStart = this.getWeekStart(refDate);
const weekEnd = this.getWeekEnd(weekStart);
const weekStart = getWeekStart(refDate);
const weekEnd = getWeekEnd(weekStart);
//fetches all shifts containing hours
const shifts = await this.prisma.shifts.findMany({
@ -63,7 +35,7 @@ export class OvertimeService {
});
//calculate total hours of those shifts minus weekly Max to find total overtime hours
const total = shifts.map(shift => this.computedHours(shift.start_time, shift.end_time))
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.weeklyMax);

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
@Injectable()
export class SickLeaveService {
@ -7,10 +8,10 @@ export class SickLeaveService {
private readonly logger = new Logger(SickLeaveService.name);
async calculateSickLeavePay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise<number> {
//sets the year to jan 1st to dec 31st
const periodStart = new Date(startDate.getFullYear(), 0, 1);
const periodEnd = startDate;
const periodStart = getYearStart(referenceDate);
const periodEnd = referenceDate;
//fetches all shifts of a selected employee
const shifts = await this.prisma.shifts.findMany({
@ -54,7 +55,7 @@ export class SickLeaveService {
const payableDays = Math.min(acquiredDays, daysRequested);
const rawHours = payableDays * 8 * modifier;
const rounded = Math.round(rawHours * 4) / 4;
const rounded = roundToQuarterHour(rawHours)
this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`);
return rounded;
}

View File

@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
import { PrismaService } from "src/prisma/prisma.service";
import { computeHours } from "src/common/utils/date-utils";
@Injectable()
export class PayPeriodsOverviewService {
@ -40,7 +41,7 @@ export class PayPeriodsOverviewService {
const user = employee_record.user;
const employee_id = employee_record.user_id;
const employee_name = `${user.first_name} ${user.last_name}`;
const hours = (shift.end_time.getTime() - shift.start_time.getTime() / 3600000);
const hours = computeHours(shift.start_time, shift.end_time);
//check if employee had prior shifts and adds hours of found shift to the total hours
if (map.has(employee_id)) {

View File

@ -1,4 +1,5 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { computeHours } from "src/common/utils/date-utils";
import { PrismaService } from "src/prisma/prisma.service";
export interface ValidationRow {
@ -16,12 +17,6 @@ export interface ValidationRow {
export class ShiftsValidationService {
constructor(private readonly prisma: PrismaService) {}
private computeHours(start: Date, end: Date): number {
const diffMs = end.getTime() - start.getTime();
const hours = diffMs / 1000 / 3600;
return parseFloat(hours.toFixed(2));
}
async getSummary(periodId: number): Promise<ValidationRow[]> {
//fetch pay-period to display
const period = await this.prisma.payPeriods.findUnique({
@ -77,7 +72,7 @@ export class ShiftsValidationService {
isValidated: false,
};
}
const hours = this.computeHours(s.start_time, s.end_time);
const hours = computeHours(s.start_time, s.end_time);
switch(s.bank_code.type) {
case 'regular' : row.totalRegularHrs += hours;

View File

@ -4,6 +4,7 @@ import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets, TimesheetsArchive } from '@prisma/client';
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
import { computeHours } from 'src/common/utils/date-utils';
@Injectable()
export class TimesheetsService {
@ -35,7 +36,7 @@ export class TimesheetsService {
return Promise.all(
list.map(async timesheet => {
const detailedShifts = timesheet.shift.map(s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const hours = computeHours(s.start_time, s.end_time,5);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;
@ -65,7 +66,7 @@ export class TimesheetsService {
}
const detailedShifts = timesheet.shift.map( s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const hours = computeHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;