import { toDateFromString, toStringFromHHmm, toStringFromDate, toDateFromHHmm, overlaps, computeHours } from "src/common/utils/date-utils"; import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { Result } from "src/common/errors/result-error.factory"; 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 { constructor( private readonly prisma: PrismaService, private readonly typeResolver: BankCodesResolver, private readonly timesheetResolver: EmployeeTimesheetResolver, private readonly emailResolver: EmailToIdResolver, private readonly paidTimeOffService: PaidTimeOFfBankHoursService, ) { } async updateOneOrManyShifts(shifts: ShiftDto[], email: string): Promise> { try { //verify if array is empty or not if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; //check for overlap inside dto objects const overlap_check = await this.overlapChecker(shifts); if (!overlap_check.success) return overlap_check; //calls the update functions and await the return of successfull result or not const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift, email))); //return arrays of updated shifts or errors const updated_shifts: ShiftDto[] = []; const errors: string[] = []; //filters results into updated_shifts or errors arrays depending on the return from "allSettled" Promise for (const result of results) { if (result.status === 'fulfilled') { if (result.value.success) { updated_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 updated and returns an array of errors if needed if (updated_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift updated' }; // returns array of updated shifts return { success: true, data: true } } catch (error) { return { success: false, error } } } //_________________________________________________________________ // UPDATE //_________________________________________________________________ async updateShift(dto: ShiftDto, email: string): Promise> { try { 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 }, select: shift_select, }); if (!original) return { success: false, error: `SHIFT_NOT_FOUND` }; //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` }; //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_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({ where: { id: original.id }, data: { date: normed_shift.data.date, start_time: normed_shift.data.start_time, end_time: normed_shift.data.end_time, bank_code_id: bank_code.data, comment: dto.comment, is_approved: dto.is_approved, is_remote: dto.is_remote, }, select: shift_select, }); if (!updated) return { success: false, error: 'INVALID_SHIFT' }; // builds an object to return for display in the frontend const shift: ShiftDto = { id: updated.id, timesheet_id: updated.timesheet_id, type: dto.type, date: toStringFromDate(updated.date), start_time: toStringFromHHmm(updated.start_time), end_time: toStringFromHHmm(updated.end_time), is_approved: updated.is_approved, is_remote: updated.is_remote, comment: updated.comment ?? '', } return { success: true, data: shift }; } catch (error) { return { success: false, error: `INVALID_SHIFT` }; } } //_________________________________________________________________ // helpers //_________________________________________________________________ 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' } return { success: true, data: { date: toDateFromString(dto.date), start_time: toDateFromHHmm(dto.start_time), end_time: toDateFromHHmm(dto.end_time), bank_code_id: bank_code_id.data } }; } private overlapChecker = async (shifts: ShiftDto[]): Promise> => { for (let i = 0; i < shifts.length; i++) { for (let j = i + 1; j < shifts.length; j++) { const shift_a = shifts[i]; const shift_b = shifts[j]; if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue; const has_overlap = overlaps( { start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) }, { start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) }, ); if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` }; } } return { success: true, data: undefined } } }