208 lines
8.5 KiB
TypeScript
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 }
|
|
});
|
|
}
|
|
} |