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 { 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`);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user