targo-backend/src/time-and-attendance/shifts/services/shifts-create.service.ts
2026-01-08 14:20:05 -05:00

163 lines
8.5 KiB
TypeScript

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 { PrismaService } from "src/prisma/prisma.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";
@Injectable()
export class ShiftsCreateService {
constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver,
private readonly vacationService: VacationService,
) { }
//_________________________________________________________________
// CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT
//_________________________________________________________________
async createOneOrManyShifts(email: string, shifts: ShiftDto[]): 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' };
//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' };
// 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<Result<ShiftDto, string>> {
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` };
}
}
//api call to validate available hours in vacation_bank and ajust end_time accordingly
if (dto.type === 'VACATION') {
const asked_hours = computeHours(toDateFromString(dto.start_time), toDateFromString(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);
}
//ADD HERE THE LOGICS TO CHECK FOR AVAILABLE BANK TYPE "PAID_BANKED_HOUR" AND BANKING_HOUR
//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: normed_shift.data.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<Result<Normalized, string>> => {
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 } };
}
private addHourstoDateString = (start_time: string, hours: number): string => {
const start = toDateFromHHmm(start_time);
const end = new Date(start.getTime() + hours * 60 * 60 * 1000);
const hh = String(end.getUTCHours()).padStart(2, '0');
const mm = String(end.getUTCMinutes()).padStart(2, '0');
return `${hh}:${mm}:00`;
}
}