refactor(shifts): added Patch and Post route to shift module and added Overtime checks to create/update and delete functions.

This commit is contained in:
Matthieu Haineault 2025-10-21 09:33:04 -04:00
parent 7537c2ff0d
commit d1974ea9e3
4 changed files with 478 additions and 318 deletions

View File

@ -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<number> {
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<number> {
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<void> {
//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<WeekOvertimeSummary>{
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<string, number>();
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<number> {
// 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<number> {
// 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<void> {
// //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`);
// }
}

View File

@ -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<ShiftWithOvertimeDto> {
return this.upser_service.createShift(timesheet_id, dto)
}
@Patch(':shift_id')
update(
@Param('shift_id') shift_id: number,
@Body() dto: updateShiftDto): Promise<ShiftWithOvertimeDto>{
return this.upser_service.updateShift(shift_id, dto);
}
@Delete(':shift_id')
remove(@Param('shift_id') shift_id: number): Promise<Shifts>{
remove(
@Param('shift_id') shift_id: number){
return this.upser_service.deleteShift(shift_id);
}
}

View File

@ -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<ShiftWithOvertimeDto> {
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<ShiftWithOvertimeDto> {
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<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 }
});
}
}

View File

@ -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";