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, 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 { 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 { //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 } }); } }