From d1974ea9e3d7d90a090785463e8b9b29d5da9093 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 21 Oct 2025 09:33:04 -0400 Subject: [PATCH] refactor(shifts): added Patch and Post route to shift module and added Overtime checks to create/update and delete functions. --- .../services/overtime.service.ts | 345 ++++++++------ .../shifts/controllers/shift.controller.ts | 25 +- .../shifts/services/shifts-upsert.service.ts | 422 ++++++++++-------- .../~misc_deprecated-files/shifts.helpers.ts | 4 - 4 files changed, 478 insertions(+), 318 deletions(-) diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts index 24c7a75..be8c15c 100644 --- a/src/modules/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -1,152 +1,247 @@ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../../prisma/prisma.service'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; -import { Prisma } from '@prisma/client'; +import { Prisma, PrismaClient } from '@prisma/client'; + +type Tx = Prisma.TransactionClient | PrismaClient; + +export type WeekOvertimeSummary = { + week_start:string; + week_end: string; + week_total_hours: number; + weekly_overtime: number; + daily_overtime_kept: number; + total_overtime: number; + breakdown: Array<{ + date:string; + day_hours: number; + day_overtime: number; + daily_kept: number; + running_total_before: number; + }>; +}; @Injectable() export class OvertimeService { private logger = new Logger(OvertimeService.name); - private daily_max = 8; // maximum for regular hours per day - private weekly_max = 40; //maximum for regular hours per week + private daily_max = 8; // maximum for regular hours per day + private weekly_max = 40; // maximum for regular hours per week private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation constructor(private prisma: PrismaService) {} - //calculate daily overtime - async getDailyOvertimeHours(employee_id: number, date: Date): Promise { - const shifts = await this.prisma.shifts.findMany({ - where: { date: date, timesheet: { employee_id: employee_id } }, - select: { start_time: true, end_time: true }, - }); - const total = shifts.map((shift)=> - computeHours(shift.start_time, shift.end_time, 5)).reduce((sum, hours)=> sum + hours, 0); - const overtime = Math.max(0, total - this.daily_max); - - this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - return overtime; - } - - //calculate Weekly overtime - async getWeeklyOvertimeHours(employee_id: number, ref_date: Date): Promise { - const week_start = getWeekStart(ref_date); - const week_end = getWeekEnd(week_start); - - //fetches all shifts from INCLUDED_TYPES array - const included_shifts = await this.prisma.shifts.findMany({ - where: { - date: { gte:week_start, lte: week_end }, - timesheet: { employee_id }, - bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - }, - select: { start_time: true, end_time: true }, - orderBy: [{date: 'asc'}, {start_time:'asc'}], - }); - - //calculate total hours of those shifts minus weekly Max to find total overtime hours - const total = included_shifts.map(shift => computeHours(shift.start_time, shift.end_time, 5)) - .reduce((sum, hours)=> sum+hours, 0); - const overtime = Math.max(0, total - this.weekly_max); - - this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - return overtime; - } - - //transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift - async transformRegularHoursToWeeklyOvertime( - employee_id: number, - ref_date: Date, - tx?: Prisma.TransactionClient, - ): Promise { - //ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected. + async getWeekOvertimeSummary( timesheet_id: number, date: Date, tx?: Tx ): Promise{ const db = tx ?? this.prisma; - //calculate weekly overtime - const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date); - if(overtime_hours <= 0) return; - - const convert_to_minutes = Math.round(overtime_hours * 60); - - const [regular, overtime] = await Promise.all([ - db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }), - db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }), - ]); - if(!regular || !overtime) return; - - const week_start = getWeekStart(ref_date); + const week_start = getWeekStart(date); const week_end = getWeekEnd(week_start); - //gets all regular shifts and order them by desc - const regular_shifts_desc = await db.shifts.findMany({ + const shifts = await db.shifts.findMany({ where: { - date: { gte:week_start, lte: week_end }, - timesheet: { employee_id }, - bank_code_id: regular.id, + timesheet_id, + date: { gte: week_start, lte: week_end }, + bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, }, - select: { - id: true, - timesheet_id: true, - date: true, - start_time: true, - end_time: true, - is_remote: true, - comment: true, - }, - orderBy: [{date: 'desc'}, {start_time:'desc'}], + select: { date: true, start_time: true, end_time: true }, + orderBy: [{date: 'asc'}, {start_time: 'asc'}], }); - let remaining_minutes = convert_to_minutes; - - for(const shift of regular_shifts_desc) { - if(remaining_minutes <= 0) break; - - const start = shift.start_time; - const end = shift.end_time; - const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000)); - if(duration_in_minutes === 0) continue; - - if(duration_in_minutes <= remaining_minutes) { - await db.shifts.update({ - where: { id: shift.id }, - data: { bank_code_id: overtime.id }, - }); - remaining_minutes -= duration_in_minutes; - continue; - } - //sets the start_time of the new overtime shift - const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000); - - //shorten the regular shift - await db.shifts.update({ - where: { id: shift.id }, - data: { end_time: new_overtime_start }, - }); - - //creates the new overtime shift to replace the shorten regular shift - await db.shifts.create({ - data: { - timesheet_id: shift.timesheet_id, - date: shift.date, - start_time: new_overtime_start, - end_time: end, - is_remote: shift.is_remote, - comment: shift.comment, - bank_code_id: overtime.id, - }, - }); - remaining_minutes = 0; + const day_totals = new Map(); + for (const shift of shifts){ + const key = shift.date.toISOString().slice(0,10); + const hours = computeHours(shift.start_time, shift.end_time, 5); + day_totals.set(key, (day_totals.get(key) ?? 0) + hours); } - this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id} - week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)} - converted= ${(convert_to_minutes-remaining_minutes)/60}h`); + + const days: string[] = []; + for(let i = 0; i < 7; i++){ + const day = new Date(week_start.getTime() + i * 24 * 60 * 60 * 1000); + days.push(day.toISOString().slice(0,10)); + } + + const week_total_hours = [ ...day_totals.values()].reduce((a,b) => a + b, 0); + const weekly_overtime = Math.max(0, week_total_hours - this.weekly_max); + + let running = 0; + let daily_kept_sum = 0; + const breakdown: WeekOvertimeSummary['breakdown'] = []; + + for (const key of days) { + const day_hours = day_totals.get(key) ?? 0; + const day_overtime = Math.max(0, day_hours - this.daily_max); + + const cap_before_40 = Math.max(0, this.weekly_max - running); + const daily_kept = Math.min(day_overtime, cap_before_40); + + breakdown.push({ + date: key, + day_hours, + day_overtime, + daily_kept, + running_total_before: running, + }); + + daily_kept_sum += daily_kept; + running += day_hours; + } + const total_overtime = weekly_overtime + daily_kept_sum; + + this.logger.debug( + `[OVERTIME][SUMMARY][ts=${timesheet_id}] week=${week_start.toISOString().slice(0,10)}..${week_end + .toISOString() + .slice(0,10)} week_total=${week_total_hours.toFixed(2)}h weekly=${weekly_overtime.toFixed( + 2, + )}h daily_kept=${daily_kept_sum.toFixed(2)}h total=${total_overtime.toFixed(2)}h`, + ); + return { + week_start: week_start.toISOString().slice(0, 10), + week_end: week_end.toISOString().slice(0, 10), + week_total_hours, + weekly_overtime, + daily_overtime_kept: daily_kept_sum, + total_overtime, + breakdown, + }; } - //apply modifier to overtime hours - // calculateOvertimePay(overtime_hours: number, modifier: number): number { - // const pay = overtime_hours * modifier; - // this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtime_hours}, modifier ${modifier})`); + // //calculate daily overtime + // async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise { + // const shifts = await this.prisma.shifts.findMany({ + // where: { + // timesheet_id, + // date: date, + // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, + // }, + // select: { start_time: true, end_time: true }, + // orderBy: [{ start_time: 'asc' }], + // }); - // return pay; + // const total = shifts.map((shift)=> + // computeHours(shift.start_time, shift.end_time, 5)). + // reduce((sum, hours)=> sum + hours, 0); + + // const overtime = Math.max(0, total - this.daily_max); + + // this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); + // return overtime; + // } + + // //calculate Weekly overtime + // async getWeeklyOvertimeHours(timesheet_id: number, ref_date: Date): Promise { + // const week_start = getWeekStart(ref_date); + // const week_end = getWeekEnd(week_start); + + // //fetches all shifts from INCLUDED_TYPES array + // const included_shifts = await this.prisma.shifts.findMany({ + // where: { + // timesheet_id, + // date: { gte:week_start, lte: week_end }, + // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, + // }, + // select: { start_time: true, end_time: true }, + // orderBy: [{date: 'asc'}, {start_time:'asc'}], + // }); + + // //calculate total hours of those shifts minus weekly Max to find total overtime hours + // const total = included_shifts.map(shift => + // computeHours(shift.start_time, shift.end_time, 5)). + // reduce((sum, hours)=> sum+hours, 0); + + // const overtime = Math.max(0, total - this.weekly_max); + + // this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); + // return overtime; + // } + + + // //transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift + // async transformRegularHoursToWeeklyOvertime( + // employee_id: number, + // ref_date: Date, + // tx?: Prisma.TransactionClient, + // ): Promise { + // //ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected. + // const db = tx ?? this.prisma; + + // //calculate weekly overtime + // const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date); + // if(overtime_hours <= 0) return; + + // const convert_to_minutes = Math.round(overtime_hours * 60); + + // const [regular, overtime] = await Promise.all([ + // db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }), + // db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }), + // ]); + // if(!regular || !overtime) return; + + // const week_start = getWeekStart(ref_date); + // const week_end = getWeekEnd(week_start); + + // //gets all regular shifts and order them by desc + // const regular_shifts_desc = await db.shifts.findMany({ + // where: { + // date: { gte:week_start, lte: week_end }, + // timesheet: { employee_id }, + // bank_code_id: regular.id, + // }, + // select: { + // id: true, + // timesheet_id: true, + // date: true, + // start_time: true, + // end_time: true, + // is_remote: true, + // comment: true, + // }, + // orderBy: [{date: 'desc'}, {start_time:'desc'}], + // }); + + // let remaining_minutes = convert_to_minutes; + + // for(const shift of regular_shifts_desc) { + // if(remaining_minutes <= 0) break; + + // const start = shift.start_time; + // const end = shift.end_time; + // const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000)); + // if(duration_in_minutes === 0) continue; + + // if(duration_in_minutes <= remaining_minutes) { + // await db.shifts.update({ + // where: { id: shift.id }, + // data: { bank_code_id: overtime.id }, + // }); + // remaining_minutes -= duration_in_minutes; + // continue; + // } + // //sets the start_time of the new overtime shift + // const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000); + + // //shorten the regular shift + // await db.shifts.update({ + // where: { id: shift.id }, + // data: { end_time: new_overtime_start }, + // }); + + // //creates the new overtime shift to replace the shorten regular shift + // await db.shifts.create({ + // data: { + // timesheet_id: shift.timesheet_id, + // date: shift.date, + // start_time: new_overtime_start, + // end_time: end, + // is_remote: shift.is_remote, + // comment: shift.comment, + // bank_code_id: overtime.id, + // }, + // }); + // remaining_minutes = 0; + // } + // this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id} + // week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)} + // converted= ${(convert_to_minutes-remaining_minutes)/60}h`); // } } diff --git a/src/modules/shifts/controllers/shift.controller.ts b/src/modules/shifts/controllers/shift.controller.ts index e57c233..edef7c8 100644 --- a/src/modules/shifts/controllers/shift.controller.ts +++ b/src/modules/shifts/controllers/shift.controller.ts @@ -1,7 +1,8 @@ //newer version that uses Express session data -import { Controller, Delete, Param } from "@nestjs/common"; -import { ShiftsUpsertService } from "../services/shifts-upsert.service"; -import { Shifts } from "@prisma/client"; +import { Body, Controller, Delete, Param, Patch, Post } from "@nestjs/common"; +import { ShiftsUpsertService, ShiftWithOvertimeDto } from "../services/shifts-upsert.service"; +import { updateShiftDto } from "../dtos/update-shift.dto"; +import { ShiftDto } from "../dtos/shift.dto"; @Controller('shift') @@ -10,8 +11,24 @@ export class ShiftController { private readonly upser_service: ShiftsUpsertService, ){} + @Post(':timesheet_id') + create( + @Param('timesheet_id') timesheet_id: number, + @Body()dto: ShiftDto): Promise { + return this.upser_service.createShift(timesheet_id, dto) + } + + @Patch(':shift_id') + update( + @Param('shift_id') shift_id: number, + @Body() dto: updateShiftDto): Promise{ + return this.upser_service.updateShift(shift_id, dto); + } + @Delete(':shift_id') - remove(@Param('shift_id') shift_id: number): Promise{ + remove( + @Param('shift_id') shift_id: number){ return this.upser_service.deleteShift(shift_id); } + } \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-upsert.service.ts b/src/modules/shifts/services/shifts-upsert.service.ts index e3adc52..bd3b822 100644 --- a/src/modules/shifts/services/shifts-upsert.service.ts +++ b/src/modules/shifts/services/shifts-upsert.service.ts @@ -1,21 +1,255 @@ 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 { OvertimeService, WeekOvertimeSummary } from "src/modules/business-logics/services/overtime.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"; +import { GetFindResult } from "@prisma/client/runtime/library"; type Normalized = { date: Date; start_time: Date; end_time: Date; }; +export type ShiftWithOvertimeDto = { + shift: GetShiftDto; + overtime: WeekOvertimeSummary; +}; + @Injectable() export class ShiftsUpsertService { constructor( private readonly prisma: PrismaService, - private readonly getService: ShiftsGetService, + private readonly overtime: OvertimeService, ){} + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + //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 + //calculate overtime + //return an object of type GetShiftDto for the frontend to display + async createShift(timesheet_id: number, dto: ShiftDto): Promise { + const normed_shift = this.normalizeShiftDto(dto); + if(normed_shift.end_time <= normed_shift.start_time){ + throw new BadRequestException('end_time must be greater than start_time') + } + + //create the shift with normalized date and times + const {row, summary } = await this.prisma.$transaction(async (tx) => { + const conflict = await tx.shifts.findFirst({ + where: { + timesheet_id, + date: normed_shift.date, + NOT: [ + { end_time: { lte: normed_shift.start_time } }, + { start_time: { gte: normed_shift.end_time } }, + ], + }, + select: { + start_time: true, + end_time: true, + bank_code: { select: { type: true } }, + }, + }); + if(conflict) { + throw new ConflictException({ + error_code: 'SHIFT_OVERLAP', + message: 'New shift overlaps with existing shift(s)', + conflicts: [{ + start_time: toStringFromHHmm(conflict.start_time), + end_time: toStringFromHHmm(conflict.end_time), + type: conflict.bank_code.type ?? 'UNKNWON', + }], + }); + } + + const row = await tx.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, + is_approved: true, + comment: true, + }, + }); + + const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id , normed_shift.date, tx); + + return {row, summary}; + }); + + const shift: GetShiftDto = { + timesheet_id: row.timesheet_id, + bank_code_id: row.bank_code_id, + date: toStringFromDate(row.date), + start_time: toStringFromHHmm(row.start_time), + end_time: toStringFromHHmm(row.end_time), + is_remote: row.is_remote, + is_approved: false, + comment: row.comment ?? undefined, + }; + + return {shift ,overtime: summary }; + }; + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + // 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 + // recalculate overtime after update + // return an updated version to display + async updateShift(shift_id: number, dto: updateShiftDto): Promise { + const {row, existing, summary_new } = await this.prisma.$transaction(async (tx) =>{ + //search for original shift using shift_id + const existing = await tx.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 normed_shift: Normalized = { + date: toDateFromString(date_string), + start_time: toHHmmFromString(start_string), + end_time: toHHmmFromString(end_string), + }; + if(normed_shift.end_time <= normed_shift.start_time) throw new BadRequestException('end time must be greater than start time'); + + const conflict = await tx.shifts.findFirst({ + where: { + timesheet_id: existing.timesheet_id, + date: normed_shift.date, + id: { not: shift_id }, + NOT: [ + { end_time: { lte: normed_shift.start_time } }, + { start_time: { gte: normed_shift.end_time } }, + ], + }, + select: { + start_time: true, + end_time: true, + bank_code: { select: { type: true } } + }, + }); + if(conflict) { + throw new ConflictException({ + error_code: 'SHIFT_OVERLAP', + message: 'New shift overlaps with existing shift(s)', + conflicts: [{ + start_time: toStringFromHHmm(conflict.start_time), + end_time: toStringFromHHmm(conflict.end_time), + type: conflict.bank_code?.type ?? 'UNKNOWN', + }], + }); + } + + //partial build, update only modified datas + const data: any = {}; + if(dto.date !== undefined) data.date = normed_shift.date; + if(dto.start_time !== undefined) data.start_time = normed_shift.start_time; + if(dto.end_time !== undefined) data.end_time = normed_shift.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; + + const row = await tx.shifts.update({ + //sends updated data to DB + 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, + }, + }); + + const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, existing.date, tx); + if(row.date.getTime() !== existing.date.getTime()) { + await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx); + } + return {row, existing, summary_new}; + }); + + const shift: GetShiftDto = { + timesheet_id: row.timesheet_id, + bank_code_id: row.bank_code_id, + date: toStringFromDate(row.date), + start_time: toStringFromHHmm(row.start_time), + end_time: toStringFromHHmm(row.end_time), + is_approved: row.is_approved, + is_remote: row.is_remote, + comment: row.comment ?? undefined, + } + //returns updated shift to frontend + return { shift, overtime: summary_new }; + + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + //finds a shift using shit_id + //recalc overtime shifts after delete + async deleteShift(shift_id: number) { + 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) throw new NotFoundException(`Shift with id #${shift_id} not found`); + + await tx.shifts.delete({ where: { id: shift_id } }); + + const summary = await this.overtime.getWeekOvertimeSummary( shift.timesheet_id, shift.date, tx); + return { + success: true, + overtime: summary + }; + }); + } + + //_________________________________________________________________ + // LOCAL HELPERS + //_________________________________________________________________ //converts all string hours and date to Date and HHmm formats private normalizeShiftDto = (dto: ShiftDto): Normalized => { const date = toDateFromString(dto.date); @@ -23,186 +257,4 @@ export class ShiftsUpsertService { 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 } - }); - } } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts index 4c417c2..a0e1e1c 100644 --- a/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts +++ b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts @@ -1,9 +1,5 @@ // import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common"; // import { Prisma, Shifts } from "@prisma/client"; -// import { UpsertShiftDto } from "../deprecated-files/upsert-shift.dto"; -// import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; -// import { normalizeShiftPayload } from "../utils/shifts.utils"; -// import { toStringFromHHmm, weekStartSunday } from "./shifts-date-time-helpers"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { OvertimeService } from "src/modules/business-logics/services/overtime.service";