import { Normalized } from "src/time-and-attendance/utils/type.utils"; import { Injectable } from "@nestjs/common"; import { timesheet_select } from "src/time-and-attendance/utils/selects.utils"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { Result } from "src/common/errors/result-error.factory"; import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toDateFromHHmm, computeHours } from "src/common/utils/date-utils"; 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"; import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() export class ShiftsCreateService { constructor( private readonly prisma: PrismaPostgresService, private readonly emailResolver: EmailToIdResolver, private readonly typeResolver: BankCodesResolver, private readonly vacationService: VacationService, private readonly bankingService: BankedHoursService, private readonly sickService: SickLeaveService, private readonly payPeriodEventService: PayPeriodEventService, ) { } //_________________________________________________________________ // CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT //_________________________________________________________________ async createOneOrManyShifts(email: string, shifts: ShiftDto[], is_from_timesheet: boolean = true): Promise> { try { //verify if array is empty or not if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'NO_DATA_RECEIVED' }; //verify if email is valid or not const employee_id = await this.emailResolver.findIdByEmail(email); if (!employee_id.success) return { success: false, error: employee_id.error }; //calls the create functions and await the return of successfull result or not const results = await Promise.allSettled(shifts.map(shift => this.createShift(employee_id.data, shift))); //return arrays of created shifts or errors const created_shifts: ShiftDto[] = []; const errors: string[] = []; //filters results into created_shifts or errors arrays depending on the return from "allSettled" Promise for (const result of results) { if (result.status === 'fulfilled') { if (result.value.success) { created_shifts.push(result.value.data); } else { errors.push(result.value.error); } } else { errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason)); } } //verify if shifts were created and returns an array of errors if needed if (created_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift created' }; // push to event service to notify timesheet-approval subscribers of change if (is_from_timesheet) { this.payPeriodEventService.emit({ employee_email: email, event_type: 'shift', action: 'create' }) } // returns array of created shifts return { success: true, data: true } } catch (error) { return { success: false, error } } } //_________________________________________________________________ // CREATE //_________________________________________________________________ async createShift(employee_id: number, dto: ShiftDto): Promise> { try { //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); if (!normed_shift.success) return { success: false, error: normed_shift.error }; if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT_TIME` }; //fetch the right timesheet const timesheet = await this.prisma.timesheets.findUnique({ where: { id: dto.timesheet_id, employee_id }, select: timesheet_select, }); if (!timesheet) return { success: false, error: `INVALID_TIMESHEET` }; //finds bank_code_id using the type const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code_id.success) return { success: false, error: bank_code_id.error }; //fetchs existing shifts from DB to check for overlaps const existing_shifts = await this.prisma.shifts.findMany({ where: { timesheet_id: timesheet.id, date: normed_shift.data.date }, select: { id: true, date: true, start_time: true, end_time: true }, }); for (const existing of existing_shifts) { const existing_start = await toDateFromString(existing.start_time); const existing_end = await toDateFromString(existing.end_time); const existing_date = await toDateFromString(existing.date); const has_overlap = overlaps( { start: normed_shift.data.start_time, end: normed_shift.data.end_time, date: normed_shift.data.date }, { start: existing_start, end: existing_end, date: existing_date }, ); if (has_overlap) { return { success: false, error: `SHIFT_OVERLAP` }; } } let adjusted_end_time = normed_shift.data.end_time; if (paid_time_off_types.includes(dto.type)) { let result: Result; let amount_hours = computeHours(normed_shift.data.start_time, normed_shift.data.end_time); const banking_types: string[] = ['BANKING', 'WITHDRAW_BANKED']; if (banking_types.includes(dto.type)) { if (dto.type === 'BANKING') { amount_hours = amount_hours * 1.5; } 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 / 1.5; 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 const created_shift = await this.prisma.shifts.create({ data: { timesheet_id: timesheet.id, bank_code_id: bank_code_id.data, date: normed_shift.data.date, start_time: normed_shift.data.start_time, end_time: adjusted_end_time, is_approved: dto.is_approved, is_remote: dto.is_remote, comment: dto.comment ?? '', }, }); //builds an object to return for display in the frontend const shift: ShiftDto = { id: created_shift.id, timesheet_id: timesheet.id, type: dto.type, date: toStringFromDate(created_shift.date), start_time: toStringFromHHmm(created_shift.start_time), end_time: toStringFromHHmm(created_shift.end_time), is_approved: created_shift.is_approved, is_remote: created_shift.is_remote, comment: created_shift.comment ?? '', } return { success: true, data: shift }; } catch (error) { return { success: false, error: `INVALID_SHIFT` }; } } //_________________________________________________________________ // LOCAL HELPERS //_________________________________________________________________ //converts all string hours and date to Date and HHmm formats private normalizeShiftDto = async (dto: ShiftDto): Promise> => { const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code_id.success) return { success: false, error: 'INVALID_SHIFT' } //TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX const date = toDateFromString(dto.date); const start_time = toDateFromHHmm(dto.start_time); const end_time = toDateFromHHmm(dto.end_time); return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } }; } }