diff --git a/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts b/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts new file mode 100644 index 0000000..f9f4f3b --- /dev/null +++ b/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts @@ -0,0 +1,10 @@ + +export const paid_time_off_types: string[] = ['SICK', 'VACATION', 'BANKING', 'WITHDRAW_BANKED']; + +//used to simplify services +export const paid_time_off_mapping: Record = { + SICK: { field: 'sick_hours', invert_logic: false, operation: 'increment' }, + VACATION: { field: 'vacation_hours', invert_logic: false, operation: 'increment' }, + WITHDRAW_BANKED: { field: 'banked_hours', invert_logic: true, operation: 'increment' }, + BANKING: { field: 'banked_hours', invert_logic: false, operation: 'decrement' }, +}; \ No newline at end of file diff --git a/src/time-and-attendance/paid-time-off/paid-time-off.module.ts b/src/time-and-attendance/paid-time-off/paid-time-off.module.ts new file mode 100644 index 0000000..f814c79 --- /dev/null +++ b/src/time-and-attendance/paid-time-off/paid-time-off.module.ts @@ -0,0 +1,21 @@ +import { Module } from "@nestjs/common"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; +import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; + +@Module({ + providers: [ + PrismaService, + EmailToIdResolver, + PaidTimeOFfBankHoursService, + VacationService, + SickLeaveService, + BankedHoursService, + ], + exports: [ + PaidTimeOFfBankHoursService, + ], +}) export class PaidTimeOffModule { } \ No newline at end of file diff --git a/src/time-and-attendance/paid-time-off/paid-time-off.service.ts b/src/time-and-attendance/paid-time-off/paid-time-off.service.ts new file mode 100644 index 0000000..cd5da0a --- /dev/null +++ b/src/time-and-attendance/paid-time-off/paid-time-off.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { computeHours } from "src/common/utils/date-utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; +import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +import { paid_time_off_mapping, paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto"; + +@Injectable() +export class PaidTimeOFfBankHoursService { + constructor( + private readonly prisma: PrismaService, + private readonly bankingService: BankedHoursService, + private readonly vacationService: VacationService, + private readonly sickLeaveService: SickLeaveService, + ) { } + + //called during update function of Shifts Module + updatePaidTimeOffBankHoursWhenShiftUpdate = async ( + start_time: Date, + end_time: Date, + type: string, + employee_id: number, + og_start: Date, + og_end: Date + ): Promise> => { + const original_hours = computeHours(og_start, og_end); + const ajusted_hours = computeHours(start_time, end_time); + const diff_hours = Math.abs(ajusted_hours - original_hours); + + if (diff_hours === 0) return { success: true, data: true }; + if (!paid_time_off_types.includes(type)) return { success: false, error: 'INVALID_SHIFT_TYPE' }; + + if (ajusted_hours > original_hours) { + const validation = await this.validateAndDeductPaidTimeOff(employee_id, type, diff_hours); + if (!validation.success) return { success: false, error: validation.error }; + } else { + const restoration = await this.restorePaidTimeOffHours(employee_id, type, diff_hours); + if (!restoration.success) return { success: false, error: restoration.error }; + } + return { success: true, data: true }; + } + + validateAndDeductPaidTimeOff = async (employee_id: number, type: string, hours: number): Promise> => { + const banking_types: string[] = ['BANKING', 'WITHDRAW_BANKED']; + if (banking_types.includes(type)) { + return await this.bankingService.manageBankingHours(employee_id, hours, type); + } + + switch (type) { + case 'VACATION': { + return await this.vacationService.manageVacationHoursBank(employee_id, hours); + } + case 'SICK': { + return await this.sickLeaveService.takeSickLeaveHours(employee_id, hours); + } + default: + return { success: false, error: 'INVALID_PAID_TIME_OFF_TYPE' }; + } + } + + + restorePaidTimeOffHours = async (employee_id: number, type: string, hours: number): Promise> => { + try { + const config = paid_time_off_mapping[type]; + if (!config) return { success: false, error: 'INVALID_PAID_TIME_OFF_TYPE' } + const operation = config.invert_logic ? 'decrement' : 'increment'; + + await this.prisma.paidTimeOff.update({ + where: { employee_id }, + data: { [config.field]: { [operation]: hours } }, + }); + return { success: true, data: true }; + } catch (error) { + return { success: false, error: 'PAID_TIME_OFF_NOT_FOUND' }; + } + }; + + + //called during delete function of Shifts Module + updatePaidTimeoffBankHoursWhenShiftDelete = async (start: Date, end: Date, type: string, employee_id: number) => { + const ajusted_hours = computeHours(start, end); + if (!paid_time_off_types.includes(type)) return; + + const config = paid_time_off_mapping[type]; + await this.prisma.paidTimeOff.update({ + where: { employee_id }, + data: { + [config.field]: { [config.operation]: ajusted_hours }, + }, + }); + + } +} diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index d9fa4b7..9a7c2e5 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -10,6 +10,7 @@ import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto"; import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto"; @Injectable() export class ShiftsCreateService { @@ -100,31 +101,36 @@ export class ShiftsCreateService { } } - //api call to validate available hours in vacation_bank and ajust end_time accordingly - if (dto.type === 'VACATION') { - const asked_hours = computeHours(toDateFromHHmm(dto.start_time), toDateFromHHmm(dto.end_time)); - const vacation_shift = await this.vacationService.manageVacationHoursBank(employee_id, asked_hours) - if (!vacation_shift.success) return { success: false, error: vacation_shift.error }; - dto.end_time = this.addHourstoDateString(dto.start_time, vacation_shift.data); - } + let adjusted_end_time = normed_shift.data.end_time; - //api call to validate available hours in banked_hours and ajust end_time accordingly - const banking_types: string[] = ['BANKING', 'WITHDRAW_BANKED']; - if (banking_types.includes(dto.type)) { - const hours = computeHours(toDateFromHHmm(dto.start_time), toDateFromHHmm(dto.end_time)); - const banking_shift = await this.bankingService.manageBankingHours(employee_id, hours, dto.type); - if (!banking_shift.success) return { success: false, error: banking_shift.error }; - dto.end_time = this.addHourstoDateString(dto.start_time, banking_shift.data); - } - - //api call to validate available hours in sick_hours and ajust end_time accordingly - if (dto.type === 'SICK') { - console.log('got here') - const hours = computeHours(toDateFromHHmm(dto.start_time), toDateFromHHmm(dto.end_time)); - const sick_hours = await this.sickService.takeSickLeaveHours(employee_id, hours); - if (!sick_hours.success) return { success: false, error: sick_hours.error }; - console.log(sick_hours.data) - dto.end_time = this.addHourstoDateString(dto.start_time, sick_hours.data); + if (paid_time_off_types.includes(dto.type)) { + const amount_hours = computeHours(normed_shift.data.start_time, normed_shift.data.end_time); + const banking_types: string[] = ['BANKING', 'WITHDRAW_BANKED']; + + let result: Result; + + if (banking_types.includes(dto.type)) { + result = await this.bankingService.manageBankingHours(employee_id, amount_hours, dto.type); + } else { + switch (dto.type) { + case 'VACATION': { + result = await this.vacationService.manageVacationHoursBank(employee_id, amount_hours); + break; + } + case 'SICK': { + result = await this.sickService.takeSickLeaveHours(employee_id, amount_hours); + break; + } + default: + result = { success: false, error: 'INVALID_PAID_TIME_OFF_TYPE' }; + break; + } + } + + if (!result.success) return { success: false, error: result.error }; + const valid_hours = result.data; + adjusted_end_time = new Date(normed_shift.data.start_time); + adjusted_end_time.setHours(adjusted_end_time.getHours() + valid_hours); } //sends data for creation of a shift in db @@ -134,7 +140,7 @@ export class ShiftsCreateService { bank_code_id: bank_code_id.data, date: normed_shift.data.date, start_time: normed_shift.data.start_time, - end_time: normed_shift.data.end_time, + end_time: adjusted_end_time, is_approved: dto.is_approved, is_remote: dto.is_remote, comment: dto.comment ?? '', diff --git a/src/time-and-attendance/shifts/services/shifts-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-delete.service.ts index 3a32464..e26b00c 100644 --- a/src/time-and-attendance/shifts/services/shifts-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-delete.service.ts @@ -2,10 +2,14 @@ import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; import { computeHours } from "src/common/utils/date-utils"; import { PrismaService } from "src/prisma/prisma.service"; +import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; @Injectable() export class ShiftsDeleteService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly paidTimeOffService: PaidTimeOFfBankHoursService, + ) { } //_________________________________________________________________ // DELETE //_________________________________________________________________ @@ -15,7 +19,6 @@ export class ShiftsDeleteService { async deleteShift(shift_id: number): Promise> { try { return await this.prisma.$transaction(async (tx) => { - const paid_time_off_types: string[] = ['SICK', 'VACATION', 'BANKING', 'WITHDRAW_BANKED']; const shift = await tx.shifts.findUnique({ where: { id: shift_id }, select: { @@ -24,56 +27,26 @@ export class ShiftsDeleteService { start_time: true, end_time: true, timesheet: true, - bank_code: { select: { type: true } } + is_approved: true, + bank_code: { select: { type: true } }, }, }); if (!shift) return { success: false, error: `SHIFT_NOT_FOUND` }; - const ajusted_hours = computeHours(shift.start_time, shift.end_time); + if (shift.is_approved) return { success: false, error: 'APPROUVED_SHIFT' }; - //manage banked types, ensures update of amount of hours in bank is ajusted when a paid_time_off shift is deleted - if (paid_time_off_types.includes(shift.bank_code.type)) { - switch (shift.bank_code.type) { - case 'SICK': - await this.prisma.paidTimeOff.update({ - where: { employee_id: shift.timesheet.employee_id }, - data: { - sick_hours: { increment: ajusted_hours }, - }, - }); - break; - case 'VACATION': - await this.prisma.paidTimeOff.update({ - where: { employee_id: shift.timesheet.employee_id }, - data: { - vacation_hours: { increment: ajusted_hours }, - }, - }); - break; - case 'WITHDRAW_BANKED': - await this.prisma.paidTimeOff.update({ - where: { employee_id: shift.timesheet.employee_id }, - data: { - banked_hours: { decrement: ajusted_hours }, - }, - }); - case 'BANKING': - await this.prisma.paidTimeOff.update({ - where: { employee_id: shift.timesheet.employee_id }, - data: { - banked_hours: { increment: ajusted_hours }, - }, - }); - break; - default: - break; - } - } + //call to ajust paid_time_off hour banks + await this.paidTimeOffService.updatePaidTimeoffBankHoursWhenShiftDelete( + shift.start_time, + shift.end_time, + shift.bank_code.type, + shift.timesheet.employee_id + ); await tx.shifts.delete({ where: { id: shift_id } }); return { success: true, data: shift.id }; }); } catch (error) { - return { success: false, error: `SHIFT_NOT_FOUND` } + return { success: false, error: `SHIFT_NOT_FOUND` }; } } } \ No newline at end of file diff --git a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-update.service.ts similarity index 75% rename from src/time-and-attendance/shifts/services/shifts-update-delete.service.ts rename to src/time-and-attendance/shifts/services/shifts-update.service.ts index 3d1e1d1..e81e800 100644 --- a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update.service.ts @@ -10,6 +10,8 @@ import { shift_select } from "src/time-and-attendance/utils/selects.utils"; import { Normalized } from "src/time-and-attendance/utils/type.utils"; import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; +import { paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto"; @Injectable() export class ShiftsUpdateService { @@ -18,6 +20,7 @@ export class ShiftsUpdateService { private readonly typeResolver: BankCodesResolver, private readonly timesheetResolver: EmployeeTimesheetResolver, private readonly emailResolver: EmailToIdResolver, + private readonly paidTimeOffService: PaidTimeOFfBankHoursService, ) { } async updateOneOrManyShifts(shifts: ShiftDto[], email: string): Promise> { @@ -62,12 +65,10 @@ export class ShiftsUpdateService { //_________________________________________________________________ async updateShift(dto: ShiftDto, email: string): Promise> { try { - // const paid_time_off_types: string[] = ['SICK', 'VACATION', 'BANKING', 'WITHDRAW_BANKED']; const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, toDateFromString(dto.date)); if (!timesheet.success) return { success: false, error: timesheet.error }; const employee = await this.emailResolver.findIdByEmail(email); if (!employee.success) return { success: false, error: employee.error }; - //finds original shift const original = await this.prisma.shifts.findFirst({ where: { id: dto.id, timesheet_id: timesheet.data.id }, @@ -83,51 +84,37 @@ export class ShiftsUpdateService { //finds bank_code_id using the type const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code.success) return { success: false, error: bank_code.error }; - // const original_hours = computeHours(original.start_time, original.end_time); - // const ajusted_hours = computeHours(toDateFromHHmm(dto.start_time), toDateFromHHmm(dto.end_time)); - // if (paid_time_off_types.includes(dto.type)) { - // switch (dto.type) { - // case 'SICK': - // if (ajusted_hours < original_hours){ - // const diff_hours = original_hours - ajusted_hours; - // await this.prisma.paidTimeOff.update({ - // where: { employee_id: employee.data }, - // data: { - // sick_hours: { decrement: diff_hours }, - // }, - // }); - // } else { - - // } - // break; - // case 'VACATION': - // await this.prisma.paidTimeOff.update({ - // where: { employee_id: shift.timesheet.employee_id }, - // data: { - // vacation_hours: { increment: ajusted_hours }, - // }, - // }); - // break; - // case 'WITHDRAW_BANKED': - // await this.prisma.paidTimeOff.update({ - // where: { employee_id: shift.timesheet.employee_id }, - // data: { - // banked_hours: { decrement: ajusted_hours }, - // }, - // }); - // case 'BANKING': - // await this.prisma.paidTimeOff.update({ - // where: { employee_id: shift.timesheet.employee_id }, - // data: { - // banked_hours: { increment: ajusted_hours }, - // }, - // }); - // break; - // default: - // break; - // } - // } + const original_type = original.bank_code.type; + const new_type = dto.type; + const type_changed = original_type !== new_type; + + //call to ajust paid_time_off hour banks + if (paid_time_off_types.includes(original_type) || paid_time_off_types.includes(new_type)) { + if (type_changed) { + const original_hours = computeHours(original.start_time, original.end_time); + if (paid_time_off_types.includes(original_type)) { + const restoration = await this.paidTimeOffService.restorePaidTimeOffHours(employee.data, original_type, original_hours); + if (!restoration.success) return { success: false, error: restoration.error }; + } + if (paid_time_off_types.includes(new_type)) { + const new_hours = computeHours(normed_shift.data.start_time, normed_shift.data.end_time); + const validation = await this.paidTimeOffService.validateAndDeductPaidTimeOff(employee.data, new_type, new_hours); + if (!validation.success) return { success: false, error: validation.error }; + } + } else { + const result = await this.paidTimeOffService.updatePaidTimeOffBankHoursWhenShiftUpdate( + normed_shift.data.start_time, + normed_shift.data.end_time, + dto.type, + employee.data, + original.start_time, + original.end_time + ); + if (!result.success) return { success: false, error: result.error }; + } + + } //updates sent to DB const updated = await this.prisma.shifts.update({ diff --git a/src/time-and-attendance/shifts/shift.controller.ts b/src/time-and-attendance/shifts/shift.controller.ts index f392ce1..abc4c0b 100644 --- a/src/time-and-attendance/shifts/shift.controller.ts +++ b/src/time-and-attendance/shifts/shift.controller.ts @@ -3,7 +3,7 @@ import { Modules as ModulesEnum } from ".prisma/client"; import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto"; import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service"; -import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update-delete.service"; +import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update.service"; import { ShiftsDeleteService } from "src/time-and-attendance/shifts/services/shifts-delete.service"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; diff --git a/src/time-and-attendance/shifts/shifts.module.ts b/src/time-and-attendance/shifts/shifts.module.ts index de00220..6b079cd 100644 --- a/src/time-and-attendance/shifts/shifts.module.ts +++ b/src/time-and-attendance/shifts/shifts.module.ts @@ -4,13 +4,29 @@ import { Module } from '@nestjs/common'; import { ShiftController } from 'src/time-and-attendance/shifts/shift.controller'; import { ShiftsCreateService } from 'src/time-and-attendance/shifts/services/shifts-create.service'; import { ShiftsDeleteService } from 'src/time-and-attendance/shifts/services/shifts-delete.service'; -import { ShiftsUpdateService } from 'src/time-and-attendance/shifts/services/shifts-update-delete.service'; +import { ShiftsUpdateService } from 'src/time-and-attendance/shifts/services/shifts-update.service'; import { VacationService } from 'src/time-and-attendance/domains/services/vacation.service'; import { BankedHoursService } from 'src/time-and-attendance/domains/services/banking-hours.service'; +import { PaidTimeOffModule } from 'src/time-and-attendance/paid-time-off/paid-time-off.module'; +import { ShiftsGetService } from 'src/time-and-attendance/shifts/services/shifts-get.service'; +import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-off/paid-time-off.service'; @Module({ + imports: [PaidTimeOffModule], controllers: [ShiftController], - providers: [ShiftsCreateService, ShiftsUpdateService, ShiftsDeleteService, VacationService, BankedHoursService], - exports: [ShiftsCreateService, ShiftsUpdateService, ShiftsDeleteService], + providers: [ + ShiftsCreateService, + ShiftsUpdateService, + ShiftsDeleteService, + VacationService, + BankedHoursService, + PaidTimeOFfBankHoursService, + ], + exports: [ + ShiftsCreateService, + ShiftsUpdateService, + ShiftsDeleteService, + ShiftsGetService, + ], }) export class ShiftsModule { } diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index 69fb117..d736a85 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -28,7 +28,7 @@ import { CsvExportController } from "src/time-and-attendance/exports/csv-exports import { ShiftController } from "src/time-and-attendance/shifts/shift.controller"; import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service"; import { ShiftsGetService } from "src/time-and-attendance/shifts/services/shifts-get.service"; -import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update-delete.service"; +import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update.service"; import { ShiftsDeleteService } from "src/time-and-attendance/shifts/services/shifts-delete.service"; import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; @@ -41,6 +41,8 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service"; import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; +import { PaidTimeOffModule } from "src/time-and-attendance/paid-time-off/paid-time-off.module"; +import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; @Module({ imports: [ @@ -51,6 +53,7 @@ import { BankedHoursService } from "src/time-and-attendance/domains/services/ban PayperiodsModule, CsvExportModule, SchedulePresetsModule, + PaidTimeOffModule, ], controllers: [ TimesheetController, @@ -84,6 +87,7 @@ import { BankedHoursService } from "src/time-and-attendance/domains/services/ban CsvGeneratorService, VacationService, BankedHoursService, + PaidTimeOFfBankHoursService, ], exports: [TimesheetApprovalService], }) export class TimeAndAttendanceModule { }; \ No newline at end of file