feat(business-logic): implementation of vacation.service.ts, sick-leave.service.ts and update leave-requests. service
This commit is contained in:
parent
2e6bafeb18
commit
5766715d77
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SickLeaveService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private readonly logger = new Logger(SickLeaveService.name);
|
||||||
|
|
||||||
|
async calculateSickLeavePay(employeeId: number, startDate: 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;
|
||||||
|
|
||||||
|
//fetches all shifts of a selected employee
|
||||||
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
|
where: {
|
||||||
|
timesheet: { employee_id: employeeId },
|
||||||
|
date: { gte: periodStart, lte: periodEnd},
|
||||||
|
},
|
||||||
|
select: { date: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
//count the amount of worked days
|
||||||
|
const workedDates = new Set(
|
||||||
|
shifts.map(shift => shift.date.toISOString().slice(0,10))
|
||||||
|
);
|
||||||
|
const daysWorked = workedDates.size;
|
||||||
|
this.logger.debug(`Sick leave: days worked= ${daysWorked} in ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`);
|
||||||
|
|
||||||
|
//less than 30 worked days returns 0
|
||||||
|
if (daysWorked < 30) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//default 3 days allowed after 30 worked days
|
||||||
|
let acquiredDays = 3;
|
||||||
|
|
||||||
|
//identify the date of the 30th worked day
|
||||||
|
const orderedDates = Array.from(workedDates).sort();
|
||||||
|
const thresholdDate = new Date(orderedDates[29]); // index 29 is the 30th day
|
||||||
|
|
||||||
|
//calculate each completed month, starting the 1st of the next month
|
||||||
|
const firstBonusDate = new Date(thresholdDate.getFullYear(), thresholdDate.getMonth() +1, 1);
|
||||||
|
let months = (periodEnd.getFullYear() - firstBonusDate.getFullYear()) * 12 +
|
||||||
|
(periodEnd.getMonth() - firstBonusDate.getMonth()) + 1;
|
||||||
|
if(months < 0) months = 0;
|
||||||
|
acquiredDays += months;
|
||||||
|
|
||||||
|
//cap of 10 days
|
||||||
|
if (acquiredDays > 10) acquiredDays = 10;
|
||||||
|
|
||||||
|
this.logger.debug(`Sick leave: threshold Date = ${thresholdDate.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquiredDays}`);
|
||||||
|
|
||||||
|
const payableDays = Math.min(acquiredDays, daysRequested);
|
||||||
|
const rawHours = payableDays * 8 * modifier;
|
||||||
|
const rounded = Math.round(rawHours * 4) / 4;
|
||||||
|
this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`);
|
||||||
|
return rounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VacationService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private readonly logger = new Logger(VacationService.name);
|
||||||
|
/**
|
||||||
|
* Calculate the ammount allowed for vacation days.
|
||||||
|
*
|
||||||
|
* @param employeeId employee ID
|
||||||
|
* @param startDate first day of vacation
|
||||||
|
* @param daysRequested number of days requested
|
||||||
|
* @param modifier Coefficient of hours(1)
|
||||||
|
* @returns amount of payable hours
|
||||||
|
*/
|
||||||
|
async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
|
||||||
|
//fetch hiring date
|
||||||
|
const employee = await this.prisma.employees.findUnique({
|
||||||
|
where: { id: employeeId },
|
||||||
|
select: { first_work_day: true },
|
||||||
|
});
|
||||||
|
if(!employee) {
|
||||||
|
throw new NotFoundException(`Employee #${employeeId} not found`);
|
||||||
|
}
|
||||||
|
const hireDate = employee.first_work_day;
|
||||||
|
|
||||||
|
//sets "year" to may 1st to april 30th
|
||||||
|
//check if hiring date is in may or later, we use hiring year, otherwise we use the year before
|
||||||
|
const yearOfRequest = startDate.getMonth() >= 4
|
||||||
|
? startDate.getFullYear() : startDate.getFullYear() -1;
|
||||||
|
const periodStart = new Date(yearOfRequest, 4, 1); //may = 4
|
||||||
|
const periodEnd = new Date(yearOfRequest + 1, 4, 0); //day 0 of may == april 30th
|
||||||
|
|
||||||
|
this.logger.debug(`Vacation period for request: ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`);
|
||||||
|
|
||||||
|
//steps to reach to get more vacation weeks in years
|
||||||
|
const checkpoint = [5, 10, 15];
|
||||||
|
const anniversaries = checkpoint.map(years => {
|
||||||
|
const anniversaryDate = new Date(hireDate);
|
||||||
|
anniversaryDate.setFullYear(anniversaryDate.getFullYear() + years);
|
||||||
|
return anniversaryDate;
|
||||||
|
}).filter(d => d>= periodStart && d <= periodEnd).sort((a,b) => a.getTime() - b.getTime());
|
||||||
|
|
||||||
|
this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`);
|
||||||
|
|
||||||
|
const boundaries = [periodStart, ...anniversaries,periodEnd];
|
||||||
|
//calculate prorata per segment
|
||||||
|
let totalVacationDays = 0;
|
||||||
|
const msPerDay = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
for (let i = 0; i < boundaries.length -1; i++) {
|
||||||
|
const segmentStart = boundaries[i];
|
||||||
|
const segmentEnd = boundaries[i+1];
|
||||||
|
|
||||||
|
//number of days in said segment
|
||||||
|
const daysInSegment = Math.round((segmentEnd.getTime() - segmentStart.getTime())/ msPerDay);
|
||||||
|
const yearsSinceHire = (segmentStart.getFullYear() - hireDate.getFullYear()) -
|
||||||
|
(segmentStart < new Date(segmentStart.getFullYear(), hireDate.getMonth()) ? 1 : 0);
|
||||||
|
let allocDays: number;
|
||||||
|
if(yearsSinceHire < 5) allocDays = 10;
|
||||||
|
else if(yearsSinceHire < 10) allocDays = 15;
|
||||||
|
else if(yearsSinceHire < 15) allocDays = 20;
|
||||||
|
else allocDays = 25;
|
||||||
|
|
||||||
|
//prorata for said segment
|
||||||
|
const prorata = (allocDays / 365) * daysInSegment;
|
||||||
|
totalVacationDays += prorata;
|
||||||
|
}
|
||||||
|
//compares allowed vacation pools with requested days
|
||||||
|
const payableDays = Math.min(totalVacationDays, daysRequested);
|
||||||
|
|
||||||
|
|
||||||
|
const rawHours = payableDays * 8 * modifier;
|
||||||
|
const roundedHours = Math.round(rawHours * 4) / 4;
|
||||||
|
this.logger.debug(`Vacation pay: entitledDays=${totalVacationDays.toFixed(2)}, requestedDays=${daysRequested},
|
||||||
|
payableDays=${payableDays.toFixed(2)}, hours=${roundedHours}`);
|
||||||
|
|
||||||
|
return roundedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ import { LeaveRequestController } from "./controllers/leave-requests.controller"
|
||||||
import { LeaveRequestsService } from "./services/leave-requests.service";
|
import { LeaveRequestsService } from "./services/leave-requests.service";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { HolidayService } from "src/business-logic/holiday.service";
|
import { HolidayService } from "src/business-logic/holiday.service";
|
||||||
|
import { VacationService } from "src/business-logic/vacation.service";
|
||||||
|
import { SickLeaveService } from "src/business-logic/sick-leave.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [LeaveRequestController],
|
controllers: [LeaveRequestController],
|
||||||
|
|
@ -10,6 +12,8 @@ import { HolidayService } from "src/business-logic/holiday.service";
|
||||||
LeaveRequestsService,
|
LeaveRequestsService,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
HolidayService,
|
HolidayService,
|
||||||
|
VacationService,
|
||||||
|
SickLeaveService,
|
||||||
],
|
],
|
||||||
exports: [ LeaveRequestsService],
|
exports: [ LeaveRequestsService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
|
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
|
||||||
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
||||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
|
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
|
||||||
import { HolidayService } from "src/business-logic/holiday.service";
|
import { HolidayService } from "src/business-logic/holiday.service";
|
||||||
|
import { VacationService } from "src/business-logic/vacation.service";
|
||||||
|
import { SickLeaveService } from "src/business-logic/sick-leave.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LeaveRequestsService {
|
export class LeaveRequestsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly holidayService: HolidayService,
|
private readonly holidayService: HolidayService,
|
||||||
|
private readonly vacationService: VacationService,
|
||||||
|
private readonly sickLeaveService: SickLeaveService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
|
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
|
||||||
|
|
@ -20,7 +24,9 @@ export class LeaveRequestsService {
|
||||||
data: { employee_id, bank_code_id, leave_type, start_date_time,
|
data: { employee_id, bank_code_id, leave_type, start_date_time,
|
||||||
end_date_time, comment, approval_status: approval_status ?? undefined
|
end_date_time, comment, approval_status: approval_status ?? undefined
|
||||||
},
|
},
|
||||||
include: { employee: { include: { user: true } } },
|
include: { employee: { include: { user: true } },
|
||||||
|
bank_code: true
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,18 +37,38 @@ export class LeaveRequestsService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const msPerDay = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
list.map(async request => {
|
list.map(async request => {
|
||||||
if(request.bank_code?.type === 'holiday') {
|
// end_date fallback
|
||||||
const cost = await this.holidayService.calculateHolidayPay(
|
const endDate = request.end_date_time ?? request.start_date_time;
|
||||||
request.employee_id,
|
|
||||||
request.start_date_time,
|
//Requested days
|
||||||
request.bank_code.modifier
|
const diffDays = Math.round((endDate.getTime() - request.start_date_time.getTime()) / msPerDay) +1;
|
||||||
);
|
|
||||||
return { ...request, cost };
|
// modifier fallback/validation
|
||||||
|
if (!request.bank_code || request.bank_code.modifier == null) {
|
||||||
|
throw new BadRequestException(`Modifier manquant pour bank_code_id=${request.bank_code_id}`);
|
||||||
}
|
}
|
||||||
return request;
|
const modifier = request.bank_code.modifier;
|
||||||
}),
|
|
||||||
|
let cost: number;
|
||||||
|
switch (request.bank_code.type) {
|
||||||
|
case 'holiday' :
|
||||||
|
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
|
||||||
|
break;
|
||||||
|
case 'vacation' :
|
||||||
|
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diffDays, modifier );
|
||||||
|
break;
|
||||||
|
case 'sick' :
|
||||||
|
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cost = diffDays * modifier;
|
||||||
|
}
|
||||||
|
return {...request, daysRequested: diffDays, cost };
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,17 +82,34 @@ export class LeaveRequestsService {
|
||||||
if(!request) {
|
if(!request) {
|
||||||
throw new NotFoundException(`LeaveRequest #${id} not found`);
|
throw new NotFoundException(`LeaveRequest #${id} not found`);
|
||||||
}
|
}
|
||||||
|
//validation and fallback for end_date_time
|
||||||
|
const endDate = request.end_date_time ?? request.start_date_time;
|
||||||
|
|
||||||
//search for leave type. if holiday
|
//calculate included days
|
||||||
if (request.bank_code?.type === 'holiday') {
|
const msPerDay = 1000 * 60 * 60 * 24;
|
||||||
const cost = await this.holidayService.calculateHolidayPay(
|
const diffDays = Math.floor((endDate.getTime() - request.start_date_time.getTime())/ msPerDay) + 1;
|
||||||
request.employee_id,
|
|
||||||
request.start_date_time,
|
if (!request.bank_code || request.bank_code.modifier == null) {
|
||||||
request.bank_code.modifier,
|
throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`);
|
||||||
);
|
|
||||||
return { ...request, cost };
|
|
||||||
}
|
}
|
||||||
return request;
|
const modifier = request.bank_code.modifier;
|
||||||
|
|
||||||
|
//calculate cost based on bank_code types
|
||||||
|
let cost = diffDays * modifier;
|
||||||
|
switch(request.bank_code.type) {
|
||||||
|
case 'holiday':
|
||||||
|
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
|
||||||
|
break;
|
||||||
|
case 'vacation':
|
||||||
|
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diffDays, modifier );
|
||||||
|
break;
|
||||||
|
case 'sick':
|
||||||
|
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cost = diffDays * modifier;
|
||||||
|
}
|
||||||
|
return {...request, daysRequested: diffDays, cost };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
|
|
@ -78,12 +121,12 @@ export class LeaveRequestsService {
|
||||||
return this.prisma.leaveRequests.update({
|
return this.prisma.leaveRequests.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...(employee_id !== undefined && { employee_id }),
|
...(employee_id !== undefined && { employee_id }),
|
||||||
...(leave_type !== undefined && { leave_type } ),
|
...(leave_type !== undefined && { leave_type } ),
|
||||||
...(start_date_time !== undefined && { start_date_time }),
|
...(start_date_time !== undefined && { start_date_time }),
|
||||||
...(end_date_time !== undefined && { end_date_time }),
|
...(end_date_time !== undefined && { end_date_time }),
|
||||||
...(comment !== undefined && { comment }),
|
...(comment !== undefined && { comment }),
|
||||||
...(approval_status == undefined && { approval_status }),
|
...(approval_status !== undefined && { approval_status }),
|
||||||
},
|
},
|
||||||
include: { employee: { include: { user:true } } },
|
include: { employee: { include: { user:true } } },
|
||||||
});
|
});
|
||||||
|
|
@ -112,14 +155,14 @@ export class LeaveRequestsService {
|
||||||
//copy unto archive table
|
//copy unto archive table
|
||||||
await transaction.leaveRequestsArchive.createMany({
|
await transaction.leaveRequestsArchive.createMany({
|
||||||
data: expired.map(request => ({
|
data: expired.map(request => ({
|
||||||
leave_request_id: request.id,
|
leave_request_id: request.id,
|
||||||
employee_id: request.employee_id,
|
employee_id: request.employee_id,
|
||||||
leave_type: request.leave_type,
|
leave_type: request.leave_type,
|
||||||
start_date_time: request.start_date_time,
|
start_date_time: request.start_date_time,
|
||||||
end_date_time: request.end_date_time,
|
end_date_time: request.end_date_time,
|
||||||
comment: request.comment,
|
comment: request.comment,
|
||||||
approval_status: request.approval_status,
|
approval_status: request.approval_status,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
//delete from leave_requests table
|
//delete from leave_requests table
|
||||||
await transaction.leaveRequests.deleteMany({
|
await transaction.leaveRequests.deleteMany({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user