diff --git a/src/app.module.ts b/src/app.module.ts index 1a4c3a7..b9c689c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; +import { OvertimeService } from './business-logic/overtime.service'; @Module({ imports: [ @@ -37,6 +38,6 @@ import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; PayperiodsModule, ], controllers: [AppController, HealthController], - providers: [AppService], + providers: [AppService, OvertimeService], }) export class AppModule {} diff --git a/src/business-logic/holiday.service.ts b/src/business-logic/holiday.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/business-logic/overtime.service.ts b/src/business-logic/overtime.service.ts new file mode 100644 index 0000000..b99122b --- /dev/null +++ b/src/business-logic/overtime.service.ts @@ -0,0 +1,82 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class OvertimeService { + + private logger = new Logger(OvertimeService.name); + private dailyMax = 8; // maximum for regular hours per day + private weeklyMax = 40; //maximum for regular hours per week + + 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 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 { + const weekStart = this.getWeekStart(refDate); + const weekEnd = this.getWeekEnd(weekStart); + + //fetches all shifts containing hours + const shifts = await this.prisma.shifts.findMany({ + where: { timesheet: { employee_id: employeeId, shift: { + every: {date: { gte: weekStart, lte: weekEnd } } + }, + }, + }, + select: { start_time: true, end_time: true }, + }); + + //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)) + .reduce((sum, hours)=> sum+hours, 0); + const overtime = Math.max(0, total - this.weeklyMax); + + this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`); + return overtime; + } + + //apply modifier to overtime hours + calculateOvertimePay(overtimeHours: number, modifier: number): number { + const pay = overtimeHours * modifier; + this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtimeHours}, modifier ${modifier})`); + + return pay; + } + +} diff --git a/src/business-logic/sick-leave.service.ts b/src/business-logic/sick-leave.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/business-logic/vacation.service.ts b/src/business-logic/vacation.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index c8fcff3..4cc519d 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -3,51 +3,80 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; +import { OvertimeService } from 'src/business-logic/overtime.service'; @Injectable() export class TimesheetsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly overtime: OvertimeService, + ) {} async create(dto : CreateTimesheetDto): Promise { const { employee_id, is_approved } = dto; return this.prisma.timesheets.create({ - data: { - employee_id, - is_approved: is_approved ?? false, - }, + data: { employee_id, is_approved: is_approved ?? false }, include: { - employee: { - include: { user: true } + employee: { include: { user: true } }, }, }); } - findAll(): Promise { - return this.prisma.timesheets.findMany({ - include: { - employee: { - include: { user: true }, - }, + async findAll(): Promise { + const list = await this.prisma.timesheets.findMany({ + include: { + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true } }, + employee: { include: { user : true } }, }, }); + + 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 regularHours = Math.min(8, hours); + const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); + const payRegular = regularHours * s.bank_code.modifier; + const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); + return { ...s, hours, payRegular, payOvertime }; + }); + const weeklyOvertimeHours = detailedShifts.length + ? await this.overtime.getWeeklyOvertimeHours( + timesheet.employee_id, + timesheet.shift[0].date): 0; + return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; + }) + ); } - async findOne(id: number): Promise { - const record = await this.prisma.timesheets.findUnique({ + async findOne(id: number): Promise { + const timesheet = await this.prisma.timesheets.findUnique({ where: { id }, - include: { - employee: { - include: { - user:true - } - }, - }, + include: { + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true } }, + employee: { include: { user: true } }, + }, }); - if(!record) { + if(!timesheet) { throw new NotFoundException(`Timesheet #${id} not found`); } - return record; + + const detailedShifts = timesheet.shift.map( s => { + const hours = this.overtime.computedHours(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; + const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); + return { ...s, hours, payRegular, payOvertime }; + }); + const weeklyOvertimeHours = detailedShifts.length + ? await this.overtime.getWeeklyOvertimeHours( + timesheet.employee_id, + timesheet.shift[0].date): 0; + return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; } async update(id: number, dto:UpdateTimesheetDto): Promise { @@ -59,19 +88,14 @@ export class TimesheetsService { ...(employee_id !== undefined && { employee_id }), ...(is_approved !== undefined && { is_approved }), }, - include: { - employee: { - include: { user: true } - }, + include: { employee: { include: { user: true } }, }, }); } async remove(id: number): Promise { await this.findOne(id); - return this.prisma.timesheets.delete({ - where: { id }, - }); + return this.prisma.timesheets.delete({ where: { id } }); } @@ -85,8 +109,7 @@ export class TimesheetsService { await this.prisma.$transaction(async transaction => { //fetches all timesheets to cutoff const oldSheets = await transaction.timesheets.findMany({ - where: { shift: { every: { date: { lt: cutoff } }, - }, + where: { shift: { every: { date: { lt: cutoff } } }, }, select: { id: true, @@ -106,14 +129,10 @@ export class TimesheetsService { })); //copying data from timesheets table to archive table - await transaction.timesheetsArchive.createMany({ - data: archiveDate, - }); + await transaction.timesheetsArchive.createMany({ data: archiveDate }); //removing data from timesheets table - await transaction.timesheets.deleteMany({ - where: { id: { in: oldSheets.map(s => s.id) } }, - }); + await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } }); }); }