targo-backend/src/modules/shifts/services/shifts-upsert.service.ts

208 lines
8.5 KiB
TypeScript

import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers";
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
import { ShiftsGetService } from "./shifts-get.service";
import { updateShiftDto } from "../dtos/update-shift.dto";
import { PrismaService } from "src/prisma/prisma.service";
import { GetShiftDto } from "../dtos/get-shift.dto";
import { ShiftDto } from "../dtos/shift.dto";
import { Shifts } from "@prisma/client";
type Normalized = { date: Date; start_time: Date; end_time: Date; };
@Injectable()
export class ShiftsUpsertService {
constructor(
private readonly prisma: PrismaService,
private readonly getService: ShiftsGetService,
){}
//converts all string hours and date to Date and HHmm formats
private normalizeShiftDto = (dto: ShiftDto): Normalized => {
const date = toDateFromString(dto.date);
const start_time = toHHmmFromString(dto.start_time);
const end_time = toHHmmFromString(dto.end_time);
return { date, start_time, end_time };
}
// used to compare shifts and detect overlaps between them
private overlaps = (
a_start: number,
a_end: number,
b_start: number,
b_end: number,
) => a_start < b_end && b_start < a_end;
//checked if a new shift overlaps already existing shifts
private assertNoOverlap = async (
day_shifts: Array<Shifts & { bank_code: { type: string } | null }>,
new_norm: Normalized | undefined,
exclude_id?: number,
) => {
if (!new_norm) return;
const conflicts = day_shifts.filter((shift) => {
if (exclude_id && shift.id === exclude_id) return false;
return this.overlaps(
new_norm.start_time.getTime(),
new_norm.end_time.getTime(),
shift.start_time.getTime(),
shift.end_time.getTime(),
);
});
if (conflicts.length) {
const payload = conflicts.map((shift) => ({
start_time: toStringFromHHmm(shift.start_time),
end_time: toStringFromHHmm(shift.end_time),
type: shift.bank_code?.type ?? 'UNKNOWN',
}));
throw new ConflictException({
error_code: 'SHIFT_OVERLAP',
message: 'New shift overlaps with existing shift(s)',
conflicts: payload,
});
}
}
//normalized frontend data to match DB
//loads all shift from a selected day to check for overlaping shifts
//checks for overlaping shifts
//create a new shifts
//return an object of type GetShiftDto for the frontend to display
async createShift(timesheet_id: number, dto: ShiftDto): Promise<GetShiftDto> {
const normed_shift = await this.normalizeShiftDto(dto);
if(normed_shift.end_time <= normed_shift.start_time){
throw new BadRequestException('end_time must be greater than start_time')
}
//call to a function to load all shifts contain in single day
const day_shifts = await this.getService.loadShiftsFromSameDay(timesheet_id, normed_shift.date);
//call to a function to detect overlaps between shifts
await this.assertNoOverlap( day_shifts, normed_shift )
//create the shift with normalized date and times
const shift = await this.prisma.shifts.create({
data: {
timesheet_id,
bank_code_id: dto.bank_code_id,
date: normed_shift.date,
start_time: normed_shift.start_time,
end_time: normed_shift.end_time,
is_remote: dto.is_remote,
comment: dto.comment ?? undefined,
},
select: {
timesheet_id: true,
bank_code_id: true,
date: true,
start_time: true,
end_time: true,
is_remote: true,
comment: true,
},
});
if(!shift) throw new BadRequestException(`a shift cannot be created, missing value(s).`);
return {
timesheet_id: shift.timesheet_id,
bank_code_id: shift.bank_code_id,
date: toStringFromDate(shift.date),
start_time: toStringFromHHmm(shift.start_time),
end_time: toStringFromHHmm(shift.end_time),
is_remote: shift.is_remote,
is_approved: false,
comment: shift.comment ?? undefined,
};
}
//finds existing shift in DB
//verify if shift is already approved
//normalized Date and Time format to string
//check for valid start and end times
//check for overlaping possibility
//buil a set of data to manipulate modified data only
//update shift in DB and return an updated version to display
async updateShift(shift_id: number, dto: updateShiftDto): Promise<GetShiftDto> {
//search for original shift using shift_id
const existing = await this.prisma.shifts.findUnique({
where: { id: shift_id },
select: {
id: true,
timesheet_id: true,
bank_code_id: true,
date: true,
start_time: true,
end_time: true,
is_remote: true,
is_approved: true,
comment: true,
},
});
if(!existing) throw new NotFoundException(`Shift with id: ${shift_id} not found`);
if(existing.is_approved) throw new BadRequestException('Approved shift cannot be updated');
const date_string = dto.date ?? toStringFromDate(existing.date);
const start_string = dto.start_time ?? toStringFromHHmm(existing.start_time);
const end_string = dto.end_time ?? toStringFromHHmm(existing.end_time);
const norm: Normalized = {
date: toDateFromString(date_string),
start_time: toHHmmFromString(start_string),
end_time: toHHmmFromString(end_string),
};
if(norm.end_time <= norm.start_time) throw new BadRequestException('end time must be greater than start time');
//call to a function to detect overlaps between shifts
const day_shifts = await this.getService.loadShiftsFromSameDay(existing.timesheet_id, norm.date);
//call to a function to detect overlaps between shifts
await this.assertNoOverlap(day_shifts, norm, shift_id);
//partial build, update only modified datas
const data: any = {};
if(dto.date !== undefined) data.date = norm.date;
if(dto.start_time !== undefined) data.start_time = norm.start_time;
if(dto.end_time !== undefined) data.end_time = norm.end_time;
if(dto.bank_code_id !== undefined) data.bank_code_id = dto.bank_code_id;
if(dto.is_remote !== undefined) data.is_remote = dto.is_remote;
if(dto.comment !== undefined) data.comment = dto.comment ?? null;
//sends updated data to DB
const updated_shift = await this.prisma.shifts.update({
where: { id: shift_id },
data,
select: {
timesheet_id: true,
bank_code_id: true,
date: true,
start_time: true,
end_time: true,
is_remote: true,
is_approved: true,
comment: true,
},
});
//returns updated shift to frontend
return {
timesheet_id: updated_shift.timesheet_id,
bank_code_id: updated_shift.bank_code_id,
date: toStringFromDate(updated_shift.date),
start_time: toStringFromHHmm(updated_shift.start_time),
end_time: toStringFromHHmm(updated_shift.end_time),
is_approved: updated_shift.is_approved,
is_remote: updated_shift.is_remote,
comment: updated_shift.comment ?? undefined,
};
}
async deleteShift(shift_id: number) {
const shift = await this.prisma.shifts.findUnique({
where: { id: shift_id },
select: { id: true },
});
if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`);
return this.prisma.shifts.delete({
where: { id: shift.id }
});
}
}