import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils"; import { Injectable, Logger } from "@nestjs/common"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { Result } from "src/common/errors/result-error.factory"; import { Prisma } from "@prisma/client"; @Injectable() export class SickLeaveService { constructor(private readonly prisma: PrismaPostgresService) { } async updateSickLeaveHours(employee_id: number): Promise> { const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; const FOURTEEN_DAYS = 1000 * 60 * 60 * 24 * 14; const today = new Date(); // get employee info const employee = await this.prisma.employees.findUnique({ where: { id: employee_id }, select: { first_work_day: true, last_work_day: true, id: true, } }) if (!employee) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; // check if employee has been inactive for more than 2 weeks if (employee.last_work_day) { if (today.getTime() - employee.last_work_day.getTime() >= FOURTEEN_DAYS) { return { success: false, error: "EMPLOYEE_NOT_ACTIVE" }; } } // get employee's PTO info, or create new details if not yet existing let pto_details: Prisma.Result; pto_details = await this.prisma.paidTimeOff.findUnique({ where: { employee_id: employee.id }, }) if (!pto_details) { const new_pto_entry = await this.createNewPTORow(employee.id, today); if (!new_pto_entry.success) return { success: false, error: "FAILED_TO_CREATE_EMPLOYEE_PTO" }; pto_details = new_pto_entry.data; } // check if employee has gotten his 3 days from completing his 30-day qualifying period from hiring date if (today.getTime() - employee.first_work_day.getTime() >= THIRTY_DAYS && employee.first_work_day.toISOString() === pto_details?.last_updated?.toISOString()) { const updated_pto = await this.addHoursToPTO(3 * 8, employee.id, today); if (!updated_pto.success) return { success: updated_pto.success, error: updated_pto.error } } const year_difference = today.getFullYear() - (pto_details!.last_updated?.getFullYear() ?? today.getFullYear()); const months_since_last_update = (today.getMonth() + year_difference * 12) - (pto_details!.last_updated?.getMonth() ?? 0); if (months_since_last_update > 0) { const updated_pto = await this.addHoursToPTO(months_since_last_update * 8, employee.id, today); if (updated_pto.success) { return { success: true, data: true } } } return { success: false, error: 'UNKNOWN_PTO_UPDATE_ERROR' }; } // create a new PTO row async createNewPTORow(employee_id: number, today: Date): Promise, string>> { try { const new_pto_entry = await this.prisma.paidTimeOff.create({ data: { employee_id: employee_id, last_updated: today, }, select: { id: true, employee_id: true, sick_hours: true, vacation_hours: true, banked_hours: true, last_updated: true, } }); return { success: true, data: new_pto_entry }; } catch (error) { return { success: false, error }; } } // add n number of sick PTO hours to employee PTO async addHoursToPTO(sick_hours: number, employee_id: number, last_updated: Date) { try { const update_pto = await this.prisma.paidTimeOff.update({ where: { employee_id, }, data: { sick_hours, last_updated, } }) return { success: true, data: update_pto }; } catch (error) { return { success: false, error }; } }; takeSickLeaveHours = async (employee_id: number, asked_hours: number): Promise> => { if (asked_hours <= 0) return { success: false, error: 'INVALID_BANKING_HOURS' }; try { const result = await this.prisma.$transaction(async (tx) => { const employee = await this.prisma.employees.findUnique({ where: { id: employee_id }, select: { id: true, paid_time_off: { select: { banked_hours: true }, }, }, }); if (!employee) { return { success: false, error: 'EMPLOYEE_NOT_FOUND' } as Result; } if (!employee.paid_time_off) { return { success: false, error: 'SICK_HOURS_BANK_NOT_FOUND' } as Result; } const sick_bank = (employee.paid_time_off.banked_hours).toNumber(); if (sick_bank <= 0) return { success: false, error: 'EMPTY_SICK_HOURS_BANK' } as Result; if (asked_hours > sick_bank) { return { success: true, data: sick_bank } as Result; } else { await tx.paidTimeOff.update({ where: { employee_id: employee.id }, data: { banked_hours: { decrement: asked_hours }, last_updated: new Date(), }, select: { banked_hours: true }, }); return { success: true, data: asked_hours } as Result; } }); return result; } catch (error) { return { success: false, error: 'INVALID_BANKING_SHIFT' }; } } //LEAVE REQUEST FUNCTION - DEPRECATED // async calculateSickLeavePay( // employee_id: number, // reference_date: Date, // days_requested: number, // hours_per_day: number, // modifier: number, // ): Promise> { // if (days_requested <= 0 || hours_per_day <= 0 || modifier <= 0) { // return { success: true, data: 0 }; // } // //sets the year to jan 1st to dec 31st // const period_start = getYearStart(reference_date); // const period_end = reference_date; // //fetches all shifts of a selected employee // const shifts = await this.prisma.shifts.findMany({ // where: { // timesheet: { employee_id: employee_id }, // date: { gte: period_start, lte: period_end }, // }, // select: { date: true }, // }); // //count the amount of worked days // const worked_dates = new Set( // shifts.map((shift) => shift.date.toISOString().slice(0, 10)), // ); // const days_worked = worked_dates.size; // //less than 30 worked days returns 0 // if (days_worked < 30) { // return { success: true, data: 0 }; // } // //default 3 days allowed after 30 worked days // let acquired_days = 3; // //identify the date of the 30th worked day // const ordered_dates = Array.from(worked_dates).sort(); // const threshold_date = new Date(ordered_dates[29]); // index 29 is the 30th day // //calculate each completed month, starting the 1st of the next month // const first_bonus_date = new Date( // threshold_date.getFullYear(), // threshold_date.getMonth() + 1, // 1, // ); // let months = // (period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 + // (period_end.getMonth() - first_bonus_date.getMonth()) + // 1; // if (months < 0) months = 0; // acquired_days += months; // //cap of 10 days // if (acquired_days > 10) acquired_days = 10; // const payable_days = Math.min(acquired_days, days_requested); // const raw_hours = payable_days * hours_per_day * modifier; // const rounded = roundToQuarterHour(raw_hours); // return { success: true, data: rounded }; // } }