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:
parent
7537c2ff0d
commit
d1974ea9e3
|
|
@ -1,152 +1,247 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../../prisma/prisma.service';
|
import { PrismaService } from '../../../prisma/prisma.service';
|
||||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
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()
|
@Injectable()
|
||||||
export class OvertimeService {
|
export class OvertimeService {
|
||||||
|
|
||||||
private logger = new Logger(OvertimeService.name);
|
private logger = new Logger(OvertimeService.name);
|
||||||
private daily_max = 8; // maximum for regular hours per day
|
private daily_max = 8; // maximum for regular hours per day
|
||||||
private weekly_max = 40; //maximum for regular hours per week
|
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
|
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation
|
||||||
|
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
//calculate daily overtime
|
async getWeekOvertimeSummary( timesheet_id: number, date: Date, tx?: Tx ): Promise<WeekOvertimeSummary>{
|
||||||
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.
|
|
||||||
const db = tx ?? this.prisma;
|
const db = tx ?? this.prisma;
|
||||||
|
|
||||||
//calculate weekly overtime
|
const week_start = getWeekStart(date);
|
||||||
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);
|
const week_end = getWeekEnd(week_start);
|
||||||
|
|
||||||
//gets all regular shifts and order them by desc
|
const shifts = await db.shifts.findMany({
|
||||||
const regular_shifts_desc = await db.shifts.findMany({
|
|
||||||
where: {
|
where: {
|
||||||
date: { gte:week_start, lte: week_end },
|
timesheet_id,
|
||||||
timesheet: { employee_id },
|
date: { gte: week_start, lte: week_end },
|
||||||
bank_code_id: regular.id,
|
bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } },
|
||||||
},
|
},
|
||||||
select: {
|
select: { date: true, start_time: true, end_time: true },
|
||||||
id: true,
|
orderBy: [{date: 'asc'}, {start_time: 'asc'}],
|
||||||
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;
|
const day_totals = new Map<string, number>();
|
||||||
|
for (const shift of shifts){
|
||||||
for(const shift of regular_shifts_desc) {
|
const key = shift.date.toISOString().slice(0,10);
|
||||||
if(remaining_minutes <= 0) break;
|
const hours = computeHours(shift.start_time, shift.end_time, 5);
|
||||||
|
day_totals.set(key, (day_totals.get(key) ?? 0) + hours);
|
||||||
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`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//apply modifier to overtime hours
|
const days: string[] = [];
|
||||||
// calculateOvertimePay(overtime_hours: number, modifier: number): number {
|
for(let i = 0; i < 7; i++){
|
||||||
// const pay = overtime_hours * modifier;
|
const day = new Date(week_start.getTime() + i * 24 * 60 * 60 * 1000);
|
||||||
// this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtime_hours}, modifier ${modifier})`);
|
days.push(day.toISOString().slice(0,10));
|
||||||
|
}
|
||||||
|
|
||||||
// return pay;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// //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' }],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 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`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
//newer version that uses Express session data
|
//newer version that uses Express session data
|
||||||
import { Controller, Delete, Param } from "@nestjs/common";
|
import { Body, Controller, Delete, Param, Patch, Post } from "@nestjs/common";
|
||||||
import { ShiftsUpsertService } from "../services/shifts-upsert.service";
|
import { ShiftsUpsertService, ShiftWithOvertimeDto } from "../services/shifts-upsert.service";
|
||||||
import { Shifts } from "@prisma/client";
|
import { updateShiftDto } from "../dtos/update-shift.dto";
|
||||||
|
import { ShiftDto } from "../dtos/shift.dto";
|
||||||
|
|
||||||
|
|
||||||
@Controller('shift')
|
@Controller('shift')
|
||||||
|
|
@ -10,8 +11,24 @@ export class ShiftController {
|
||||||
private readonly upser_service: ShiftsUpsertService,
|
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')
|
@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);
|
return this.upser_service.deleteShift(shift_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,86 +1,71 @@
|
||||||
import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers";
|
import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers";
|
||||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
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 { updateShiftDto } from "../dtos/update-shift.dto";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { GetShiftDto } from "../dtos/get-shift.dto";
|
import { GetShiftDto } from "../dtos/get-shift.dto";
|
||||||
import { ShiftDto } from "../dtos/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; };
|
type Normalized = { date: Date; start_time: Date; end_time: Date; };
|
||||||
|
|
||||||
|
export type ShiftWithOvertimeDto = {
|
||||||
|
shift: GetShiftDto;
|
||||||
|
overtime: WeekOvertimeSummary;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ShiftsUpsertService {
|
export class ShiftsUpsertService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly getService: ShiftsGetService,
|
private readonly overtime: OvertimeService,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
//converts all string hours and date to Date and HHmm formats
|
//_________________________________________________________________
|
||||||
private normalizeShiftDto = (dto: ShiftDto): Normalized => {
|
// CREATE
|
||||||
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
|
//normalized frontend data to match DB
|
||||||
//loads all shift from a selected day to check for overlaping shifts
|
//loads all shift from a selected day to check for overlaping shifts
|
||||||
//checks for overlaping shifts
|
//checks for overlaping shifts
|
||||||
//create a new shifts
|
//create a new shifts
|
||||||
|
//calculate overtime
|
||||||
//return an object of type GetShiftDto for the frontend to display
|
//return an object of type GetShiftDto for the frontend to display
|
||||||
async createShift(timesheet_id: number, dto: ShiftDto): Promise<GetShiftDto> {
|
async createShift(timesheet_id: number, dto: ShiftDto): Promise<ShiftWithOvertimeDto> {
|
||||||
const normed_shift = await this.normalizeShiftDto(dto);
|
const normed_shift = this.normalizeShiftDto(dto);
|
||||||
if(normed_shift.end_time <= normed_shift.start_time){
|
if(normed_shift.end_time <= normed_shift.start_time){
|
||||||
throw new BadRequestException('end_time must be greater than 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
|
//create the shift with normalized date and times
|
||||||
const shift = await this.prisma.shifts.create({
|
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: {
|
data: {
|
||||||
timesheet_id,
|
timesheet_id,
|
||||||
bank_code_id: dto.bank_code_id,
|
bank_code_id: dto.bank_code_id,
|
||||||
|
|
@ -97,33 +82,46 @@ export class ShiftsUpsertService {
|
||||||
start_time: true,
|
start_time: true,
|
||||||
end_time: true,
|
end_time: true,
|
||||||
is_remote: true,
|
is_remote: true,
|
||||||
|
is_approved: true,
|
||||||
comment: true,
|
comment: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if(!shift) throw new BadRequestException(`a shift cannot be created, missing value(s).`);
|
|
||||||
|
|
||||||
return {
|
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id , normed_shift.date, tx);
|
||||||
timesheet_id: shift.timesheet_id,
|
|
||||||
bank_code_id: shift.bank_code_id,
|
return {row, summary};
|
||||||
date: toStringFromDate(shift.date),
|
});
|
||||||
start_time: toStringFromHHmm(shift.start_time),
|
|
||||||
end_time: toStringFromHHmm(shift.end_time),
|
const shift: GetShiftDto = {
|
||||||
is_remote: shift.is_remote,
|
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,
|
is_approved: false,
|
||||||
comment: shift.comment ?? undefined,
|
comment: row.comment ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
//finds existing shift in DB
|
return {shift ,overtime: summary };
|
||||||
//verify if shift is already approved
|
};
|
||||||
//normalized Date and Time format to string
|
|
||||||
//check for valid start and end times
|
//_________________________________________________________________
|
||||||
//check for overlaping possibility
|
// UPDATE
|
||||||
//buil a set of data to manipulate modified data only
|
//_________________________________________________________________
|
||||||
//update shift in DB and return an updated version to display
|
// finds existing shift in DB
|
||||||
async updateShift(shift_id: number, dto: updateShiftDto): Promise<GetShiftDto> {
|
// 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
|
//search for original shift using shift_id
|
||||||
const existing = await this.prisma.shifts.findUnique({
|
const existing = await tx.shifts.findUnique({
|
||||||
where: { id: shift_id },
|
where: { id: shift_id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
@ -144,30 +142,52 @@ export class ShiftsUpsertService {
|
||||||
const start_string = dto.start_time ?? toStringFromHHmm(existing.start_time);
|
const start_string = dto.start_time ?? toStringFromHHmm(existing.start_time);
|
||||||
const end_string = dto.end_time ?? toStringFromHHmm(existing.end_time);
|
const end_string = dto.end_time ?? toStringFromHHmm(existing.end_time);
|
||||||
|
|
||||||
const norm: Normalized = {
|
const normed_shift: Normalized = {
|
||||||
date: toDateFromString(date_string),
|
date: toDateFromString(date_string),
|
||||||
start_time: toHHmmFromString(start_string),
|
start_time: toHHmmFromString(start_string),
|
||||||
end_time: toHHmmFromString(end_string),
|
end_time: toHHmmFromString(end_string),
|
||||||
};
|
};
|
||||||
if(norm.end_time <= norm.start_time) throw new BadRequestException('end time must be greater than start time');
|
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 detect overlaps between shifts
|
const conflict = await tx.shifts.findFirst({
|
||||||
const day_shifts = await this.getService.loadShiftsFromSameDay(existing.timesheet_id, norm.date);
|
where: {
|
||||||
|
timesheet_id: existing.timesheet_id,
|
||||||
//call to a function to detect overlaps between shifts
|
date: normed_shift.date,
|
||||||
await this.assertNoOverlap(day_shifts, norm, shift_id);
|
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
|
//partial build, update only modified datas
|
||||||
const data: any = {};
|
const data: any = {};
|
||||||
if(dto.date !== undefined) data.date = norm.date;
|
if(dto.date !== undefined) data.date = normed_shift.date;
|
||||||
if(dto.start_time !== undefined) data.start_time = norm.start_time;
|
if(dto.start_time !== undefined) data.start_time = normed_shift.start_time;
|
||||||
if(dto.end_time !== undefined) data.end_time = norm.end_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.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.is_remote !== undefined) data.is_remote = dto.is_remote;
|
||||||
if(dto.comment !== undefined) data.comment = dto.comment ?? null;
|
if(dto.comment !== undefined) data.comment = dto.comment ?? null;
|
||||||
|
|
||||||
|
const row = await tx.shifts.update({
|
||||||
//sends updated data to DB
|
//sends updated data to DB
|
||||||
const updated_shift = await this.prisma.shifts.update({
|
|
||||||
where: { id: shift_id },
|
where: { id: shift_id },
|
||||||
data,
|
data,
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -181,28 +201,60 @@ export class ShiftsUpsertService {
|
||||||
comment: 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
|
//returns updated shift to frontend
|
||||||
return {
|
return { shift, overtime: summary_new };
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// DELETE
|
||||||
|
//_________________________________________________________________
|
||||||
|
//finds a shift using shit_id
|
||||||
|
//recalc overtime shifts after delete
|
||||||
async deleteShift(shift_id: number) {
|
async deleteShift(shift_id: number) {
|
||||||
const shift = await this.prisma.shifts.findUnique({
|
return await this.prisma.$transaction(async (tx) =>{
|
||||||
|
const shift = await tx.shifts.findUnique({
|
||||||
where: { id: shift_id },
|
where: { id: shift_id },
|
||||||
select: { id: true },
|
select: { id: true, date: true, timesheet_id: true },
|
||||||
});
|
});
|
||||||
if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`);
|
if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`);
|
||||||
|
|
||||||
return this.prisma.shifts.delete({
|
await tx.shifts.delete({ where: { id: shift_id } });
|
||||||
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);
|
||||||
|
const start_time = toHHmmFromString(dto.start_time);
|
||||||
|
const end_time = toHHmmFromString(dto.end_time);
|
||||||
|
return { date, start_time, end_time };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
// import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common";
|
// import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common";
|
||||||
// import { Prisma, Shifts } from "@prisma/client";
|
// 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 { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||||
// import { OvertimeService } from "src/modules/business-logics/services/overtime.service";
|
// import { OvertimeService } from "src/modules/business-logics/services/overtime.service";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user