185 lines
8.8 KiB
TypeScript
185 lines
8.8 KiB
TypeScript
import { toDateFromString, toHHmmFromString, toStringFromHHmm, toStringFromDate, overlaps, toUTCDateFromString } from "src/time-and-attendance/utils/date-time.utils";
|
||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||
import { PrismaService } from "src/prisma/prisma.service";
|
||
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
|
||
import { Injectable } from "@nestjs/common";
|
||
import { Normalized } from "src/time-and-attendance/utils/type.utils";
|
||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||
import { Result } from "src/common/errors/result-error.factory";
|
||
import { EmployeeTimesheetResolver } from "src/time-and-attendance/utils/resolve-timesheet.utils";
|
||
|
||
@Injectable()
|
||
export class ShiftsUpdateDeleteService {
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly typeResolver: BankCodesResolver,
|
||
private readonly timesheetResolver: EmployeeTimesheetResolver,
|
||
) { }
|
||
|
||
async updateOneOrManyShifts(shifts: ShiftDto[], email: string): Promise<Result<ShiftDto[], 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: updated_shifts }
|
||
} 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 }
|
||
|
||
//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 with id: ${dto.id} not found` };
|
||
|
||
//transform string format to date and HHmm
|
||
const normed_shift = await this.normalizeShiftDto(dto);
|
||
if (!normed_shift.success) return { success: false, error: 'An error occured during normalization' }
|
||
if (normed_shift.data.end_time <= normed_shift.data.start_time) return {
|
||
success: false,
|
||
error: `INVALID_SHIFT - `
|
||
+ `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
|
||
+ `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
|
||
+ `date: ${toStringFromDate(normed_shift.data.date)}.`
|
||
};
|
||
|
||
//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 };
|
||
|
||
//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: ' An error occured during update, Invalid Datas' };
|
||
|
||
// 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: `An error occured during update. Invalid Data` };
|
||
}
|
||
}
|
||
|
||
//_________________________________________________________________
|
||
// DELETE
|
||
//_________________________________________________________________
|
||
//finds shifts using shit_ids
|
||
//blocs deletion if approved
|
||
async deleteShift(shift_id: number): Promise<Result<number, string>> {
|
||
try {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
const shift = await tx.shifts.findUnique({
|
||
where: { id: shift_id },
|
||
select: { id: true, date: true, timesheet_id: true },
|
||
});
|
||
if (!shift) return { success: false, error: `shift with id ${shift_id} not found ` };
|
||
|
||
await tx.shifts.delete({ where: { id: shift_id } });
|
||
return { success: true, data: shift.id };
|
||
});
|
||
} catch (error) {
|
||
return { success: false, error: `INVALID_SHIFT, shift with id ${shift_id} not found` }
|
||
}
|
||
}
|
||
|
||
//_________________________________________________________________
|
||
// 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: 'Bank_code not found' }
|
||
return {
|
||
success: true,
|
||
data: {
|
||
date: toDateFromString(dto.date),
|
||
start_time: toHHmmFromString(dto.start_time),
|
||
end_time: toHHmmFromString(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) continue;
|
||
if (shift_a.id === shift_b.id) continue;
|
||
const has_overlap = overlaps(
|
||
{ start: toHHmmFromString(shift_a.start_time), end: toHHmmFromString(shift_a.end_time) },
|
||
{ start: toHHmmFromString(shift_b.start_time), end: toHHmmFromString(shift_b.end_time) },
|
||
);
|
||
if (has_overlap) {
|
||
return {
|
||
success: false,
|
||
error: `SHIFT_OVERLAP`
|
||
+ `new shift: ${shift_a.start_time}–${shift_a.end_time} `
|
||
+ `existing shift: ${shift_b.start_time}–${shift_b.end_time} `
|
||
+ `date: ${shift_a.date})`,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return { success: true, data: undefined }
|
||
}
|
||
} |