targo-backend/src/time-and-attendance/shifts/services/shifts-update.service.ts

187 lines
9.2 KiB
TypeScript

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<Result<boolean, string>> {
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<Result<ShiftDto, string>> {
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<Result<Normalized, string>> => {
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<Result<void, string>> => {
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 }
}
}