refactor(shifts): refactored shiftCommandService and moved helpers to shifthelpersService
This commit is contained in:
parent
408e52b4f5
commit
4ff512a207
|
|
@ -3,6 +3,7 @@ import { BadRequestException, Injectable } from "@nestjs/common";
|
|||
import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { LeaveTypes } from "@prisma/client";
|
||||
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||
|
||||
@Injectable()
|
||||
export class LeaveRequestsUtils {
|
||||
|
|
@ -44,7 +45,9 @@ export class LeaveRequestsUtils {
|
|||
include: { bank_code: true },
|
||||
});
|
||||
|
||||
await this.shiftsCommand.upsertShiftsByDate(email, {
|
||||
const action: UpsertAction = existing ? 'update' : 'create';
|
||||
|
||||
await this.shiftsCommand.upsertShifts(email, action, {
|
||||
old_shift: existing
|
||||
? {
|
||||
date: yyyy_mm_dd,
|
||||
|
|
@ -86,7 +89,7 @@ export class LeaveRequestsUtils {
|
|||
});
|
||||
if (!existing) return;
|
||||
|
||||
await this.shiftsCommand.upsertShiftsByDate(email, {
|
||||
await this.shiftsCommand.upsertShifts(email, 'delete', {
|
||||
old_shift: {
|
||||
date: yyyy_mm_dd,
|
||||
start_time: hhmmFromLocal(existing.start_time),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { ShiftsCommandService } from "../shifts/services/shifts-command.service"
|
|||
import { SharedModule } from "../shared/shared.module";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { BusinessLogicsModule } from "../business-logics/business-logics.module";
|
||||
import { ShiftsHelpersService } from "../shifts/helpers/shifts.helpers";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule],
|
||||
|
|
@ -20,6 +21,7 @@ import { BusinessLogicsModule } from "../business-logics/business-logics.module"
|
|||
ExpensesCommandService,
|
||||
ShiftsCommandService,
|
||||
PrismaService,
|
||||
ShiftsHelpersService,
|
||||
],
|
||||
controllers: [PayPeriodsController],
|
||||
exports: [
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ShiftsQueryService } from "../services/shifts-query.service";
|
|||
import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto";
|
||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface";
|
||||
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||
|
||||
@ApiTags('Shifts')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -21,9 +22,9 @@ export class ShiftsController {
|
|||
@Put('upsert/:email')
|
||||
async upsert_by_date(
|
||||
@Param('email') email_param: string,
|
||||
@Body() payload: UpsertShiftDto,
|
||||
@Body() payload: UpsertShiftDto, action: UpsertAction,
|
||||
) {
|
||||
return this.shiftsCommandService.upsertShiftsByDate(email_param, payload);
|
||||
return this.shiftsCommandService.upsertShifts(email_param, action, payload);
|
||||
}
|
||||
|
||||
@Patch('approval/:id')
|
||||
|
|
@ -72,55 +73,4 @@ export class ShiftsController {
|
|||
|
||||
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
||||
}
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Post()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Create shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift created',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
// create(@Body() dto: CreateShiftDto): Promise<Shifts> {
|
||||
// return this.shiftsService.create(dto);
|
||||
// }
|
||||
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find all shifts' })
|
||||
// @ApiResponse({ status: 201, description: 'List of shifts found',type: CreateShiftDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of shifts not found' })
|
||||
// @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
// findAll(@Query() filters: SearchShiftsDto) {
|
||||
// return this.shiftsService.findAll(filters);
|
||||
// }
|
||||
|
||||
// @Get(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift found',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
// return this.shiftsService.findOne(id);
|
||||
// }
|
||||
|
||||
// @Patch(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Update shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift updated',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise<Shifts> {
|
||||
// return this.shiftsService.update(id, dto);
|
||||
// }
|
||||
|
||||
// @Delete(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Delete shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift deleted',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
// return this.shiftsService.remove(id);
|
||||
// }
|
||||
|
||||
}
|
||||
136
src/modules/shifts/helpers/shifts.helpers.ts
Normal file
136
src/modules/shifts/helpers/shifts.helpers.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common";
|
||||
import { Prisma, Shifts } from "@prisma/client";
|
||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types";
|
||||
import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils";
|
||||
import { weekStartSunday, formatHHmm } 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";
|
||||
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||
|
||||
|
||||
export type Tx = Prisma.TransactionClient;
|
||||
export type Normalized = Awaited<ReturnType<typeof normalizeShiftPayload>>;
|
||||
|
||||
export class ShiftsHelpersService {
|
||||
|
||||
constructor(
|
||||
private readonly bankTypeResolver: BankCodesResolver,
|
||||
private readonly overtimeService: OvertimeService,
|
||||
) { }
|
||||
|
||||
async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) {
|
||||
const start_of_week = weekStartSunday(date_only);
|
||||
return tx.timesheets.upsert({
|
||||
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
||||
update: {},
|
||||
create: { employee_id, start_date: start_of_week },
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
async normalizeRequired(
|
||||
raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null,
|
||||
label: 'old_shift' | 'new_shift' = 'new_shift',
|
||||
): Promise<Normalized> {
|
||||
if (!raw) throw new BadRequestException(`${label} is required`);
|
||||
const norm = await normalizeShiftPayload(raw);
|
||||
if (norm.end_time.getTime() <= norm.start_time.getTime()) {
|
||||
throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`);
|
||||
}
|
||||
return norm;
|
||||
}
|
||||
|
||||
async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise<number> {
|
||||
const found = await this.bankTypeResolver.findByType(type, tx);
|
||||
const id = found?.id;
|
||||
if (typeof id !== 'number') {
|
||||
throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
async getDayShifts(tx: Tx, timesheet_id: number, dateIso: string) {
|
||||
return tx.shifts.findMany({
|
||||
where: { timesheet_id, date: dateIso },
|
||||
include: { bank_code: true },
|
||||
orderBy: { start_time: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async assertNoOverlap(
|
||||
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((s) => {
|
||||
if (exclude_id && s.id === exclude_id) return false;
|
||||
return overlaps(
|
||||
new_norm.start_time.getTime(),
|
||||
new_norm.end_time.getTime(),
|
||||
s.start_time.getTime(),
|
||||
s.end_time.getTime(),
|
||||
);
|
||||
});
|
||||
if (conflicts.length) {
|
||||
const payload = conflicts.map((s) => ({
|
||||
start_time: formatHHmm(s.start_time),
|
||||
end_time: formatHHmm(s.end_time),
|
||||
type: s.bank_code?.type ?? 'UNKNOWN',
|
||||
}));
|
||||
throw new ConflictException({
|
||||
error_code: 'SHIFT_OVERLAP',
|
||||
message: 'New shift overlaps with existing shift(s)',
|
||||
conflicts: payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async findExactOldShift(
|
||||
tx: Tx,
|
||||
params: {
|
||||
timesheet_id: number;
|
||||
date_only: Date;
|
||||
norm: Normalized;
|
||||
bank_code_id: number;
|
||||
},
|
||||
) {
|
||||
const { timesheet_id, date_only, norm, bank_code_id } = params;
|
||||
return tx.shifts.findFirst({
|
||||
where: {
|
||||
timesheet_id,
|
||||
date: date_only,
|
||||
start_time: norm.start_time,
|
||||
end_time: norm.end_time,
|
||||
is_remote: norm.is_remote,
|
||||
is_approved: norm.is_approved,
|
||||
comment: norm.comment ?? null,
|
||||
bank_code_id,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date, action: UpsertAction) {
|
||||
// Switch regular → weekly overtime si > 40h
|
||||
await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx);
|
||||
const [daily, weekly] = await Promise.all([
|
||||
this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only),
|
||||
this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only),
|
||||
]);
|
||||
}
|
||||
|
||||
async mapDay(
|
||||
fresh: Array<Shifts & { bank_code: { type: string } | null }>,
|
||||
): Promise<DayShiftResponse[]> {
|
||||
return fresh.map((s) => ({
|
||||
start_time: formatHHmm(s.start_time),
|
||||
end_time: formatHHmm(s.end_time),
|
||||
type: s.bank_code?.type ?? 'UNKNOWN',
|
||||
is_remote: s.is_remote,
|
||||
comment: s.comment ?? null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
||||
import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils";
|
||||
import { DayShiftResponse, UpsertAction } from "../types-and-interfaces/shifts-upsert.types";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { normalizeShiftPayload } from "../utils/shifts.utils";
|
||||
import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types";
|
||||
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { Prisma, Shifts } from "@prisma/client";
|
||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { OvertimeService } from "src/modules/business-logics/services/overtime.service";
|
||||
import { formatHHmm, toDateOnly, weekStartSunday } from "../helpers/shifts-date-time-helpers";
|
||||
import { toDateOnly } from "../helpers/shifts-date-time-helpers";
|
||||
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||
import { ShiftsHelpersService } from "../helpers/shifts.helpers";
|
||||
|
||||
@Injectable()
|
||||
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||
|
|
@ -17,8 +17,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
constructor(
|
||||
prisma: PrismaService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
private readonly bankTypeResolver: BankCodesResolver,
|
||||
private readonly overtimeService: OvertimeService,
|
||||
private readonly helpersService: ShiftsHelpersService,
|
||||
) { super(prisma); }
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
|
|
@ -41,193 +40,157 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
//_____________________________________________________________________________________________
|
||||
// MASTER CRUD METHOD
|
||||
//_____________________________________________________________________________________________
|
||||
async upsertShiftsByDate(email:string, dto: UpsertShiftDto):
|
||||
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
|
||||
const { old_shift, new_shift } = dto;
|
||||
async upsertShifts(
|
||||
email: string,
|
||||
action: UpsertAction,
|
||||
dto: UpsertShiftDto,
|
||||
): Promise<{
|
||||
action: UpsertAction;
|
||||
day: DayShiftResponse[];
|
||||
}> {
|
||||
if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided');
|
||||
|
||||
if(!dto.old_shift && !dto.new_shift) {
|
||||
throw new BadRequestException('At least one of old or new shift must be provided');
|
||||
}
|
||||
|
||||
const date = new_shift?.date ?? old_shift?.date;
|
||||
const date = dto.new_shift?.date ?? dto.old_shift?.date;
|
||||
if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift");
|
||||
if (old_shift?.date
|
||||
&& new_shift?.date
|
||||
&& old_shift.date
|
||||
!== new_shift.date) throw new BadRequestException("old_shift.date and new_shift.date must be identical");
|
||||
|
||||
const date_only = toDateOnly(date);
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const start_of_week = weekStartSunday(date_only);
|
||||
|
||||
const timesheet = await tx.timesheets.upsert({
|
||||
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
||||
update: {},
|
||||
create: { employee_id, start_date: start_of_week },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
//validation/sanitation
|
||||
//resolve bank_code_id using type
|
||||
const old_norm_shift = old_shift ? await normalizeShiftPayload(old_shift) : undefined;
|
||||
if (old_norm_shift && old_norm_shift.end_time.getTime() <= old_norm_shift.start_time.getTime()) {
|
||||
throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
|
||||
if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) {
|
||||
throw new BadRequestException('old_shift.date and new_shift.date must be identical');
|
||||
}
|
||||
const old_bank_code_id: number | undefined = old_norm_shift ? (await this.bankTypeResolver.findByType(old_norm_shift.type, tx))?.id : undefined;
|
||||
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email
|
||||
|
||||
const new_norm_shift = new_shift ? await normalizeShiftPayload(new_shift) : undefined;
|
||||
if (new_norm_shift && new_norm_shift.end_time.getTime() <= new_norm_shift.start_time.getTime()) {
|
||||
throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
|
||||
if(action === 'create') {
|
||||
if(!dto.new_shift || dto.old_shift) {
|
||||
throw new BadRequestException(`Only new_shift must be provided for create`);
|
||||
}
|
||||
const new_bank_code_id: number | undefined = new_norm_shift ? (await this.bankTypeResolver.findByType(new_norm_shift.type, tx))?.id : undefined;
|
||||
|
||||
|
||||
//fetch all shifts in a single day and verify possible overlaps
|
||||
const day_shifts = await tx.shifts.findMany({
|
||||
where: { timesheet_id: timesheet.id, date: date_only },
|
||||
include: { bank_code: true },
|
||||
orderBy: { start_time: 'asc'},
|
||||
});
|
||||
|
||||
|
||||
const findExactOldShift = async ()=> {
|
||||
if(!old_norm_shift || old_bank_code_id === undefined) return undefined;
|
||||
const old_comment = old_norm_shift.comment ?? null;
|
||||
|
||||
return await tx.shifts.findFirst({
|
||||
where: {
|
||||
timesheet_id: timesheet.id,
|
||||
date: date_only,
|
||||
start_time: old_norm_shift.start_time,
|
||||
end_time: old_norm_shift.end_time,
|
||||
is_remote: old_norm_shift.is_remote,
|
||||
is_approved: old_norm_shift.is_approved,
|
||||
comment: old_comment,
|
||||
bank_code_id: old_bank_code_id,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
};
|
||||
|
||||
//checks for overlaping shifts
|
||||
const assertNoOverlap = (exclude_shift_id?: number)=> {
|
||||
if (!new_norm_shift) return;
|
||||
const overlap_with = day_shifts.filter((shift)=> {
|
||||
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
|
||||
return overlaps(
|
||||
new_norm_shift.start_time.getTime(),
|
||||
new_norm_shift.end_time.getTime(),
|
||||
shift.start_time.getTime(),
|
||||
shift.end_time.getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
if(overlap_with.length > 0) {
|
||||
const conflicts = overlap_with.map((shift)=> ({
|
||||
start_time: formatHHmm(shift.start_time),
|
||||
end_time: formatHHmm(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});
|
||||
return this.createShift(employee_id, date, dto);
|
||||
}
|
||||
};
|
||||
let action: UpsertAction;
|
||||
//_____________________________________________________________________________________________
|
||||
// DELETE
|
||||
//_____________________________________________________________________________________________
|
||||
if ( old_shift && !new_shift ) {
|
||||
if (old_bank_code_id === undefined) throw new NotFoundException(`bank code not found for old_shift.type: ${old_norm_shift?.type ?? ''}`);
|
||||
const existing = await findExactOldShift();
|
||||
if(!existing) {
|
||||
throw new NotFoundException({
|
||||
error_code: 'SHIFT_STALE',
|
||||
message: 'The shift was modified or deleted by someone else',
|
||||
});
|
||||
if(action === 'update'){
|
||||
if(!dto.old_shift || !dto.new_shift) {
|
||||
throw new BadRequestException(`Both new_shift and old_shift must be provided for update`);
|
||||
}
|
||||
await tx.shifts.delete({ where: { id: existing.id } } );
|
||||
action = 'deleted';
|
||||
return this.updateShift(employee_id, date, dto);
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
if(action === 'delete'){
|
||||
if(!dto.old_shift || dto.new_shift) {
|
||||
throw new BadRequestException('Only old_shift must be provided for delete');
|
||||
}
|
||||
return this.deleteShift(employee_id, date, dto);
|
||||
}
|
||||
throw new BadRequestException(`Unknown action: ${action}`);
|
||||
}
|
||||
|
||||
//_________________________________________________________________
|
||||
// CREATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if (!old_shift && new_shift) {
|
||||
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
|
||||
assertNoOverlap();
|
||||
//_________________________________________________________________
|
||||
private async createShift(
|
||||
employee_id: number,
|
||||
date_iso: string,
|
||||
dto: UpsertShiftDto,
|
||||
): Promise<{action: UpsertAction; day: DayShiftResponse[]}> {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const date_only = toDateOnly(date_iso);
|
||||
const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only);
|
||||
|
||||
const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift);
|
||||
const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift');
|
||||
|
||||
const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_iso);
|
||||
|
||||
await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift);
|
||||
|
||||
await tx.shifts.create({
|
||||
data: {
|
||||
timesheet_id: timesheet.id,
|
||||
timesheet_id,
|
||||
date: date_only,
|
||||
start_time: new_norm_shift!.start_time,
|
||||
end_time: new_norm_shift!.end_time,
|
||||
is_remote: new_norm_shift!.is_remote,
|
||||
comment: new_norm_shift!.comment ?? null,
|
||||
bank_code_id: new_bank_code_id!,
|
||||
},
|
||||
});
|
||||
action = 'created';
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// UPDATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if (old_shift && new_shift){
|
||||
if (old_bank_code_id === undefined) throw new NotFoundException(`bank code not found for old_shift.type: ${old_norm_shift?.type ?? ''}`);
|
||||
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
|
||||
const existing = await findExactOldShift();
|
||||
if(!existing) throw new NotFoundException({ error_code: 'SHIFT_STALE', message: 'The shift was modified or deleted by someone else'});
|
||||
assertNoOverlap(existing.id);
|
||||
|
||||
await tx.shifts.update({
|
||||
where: {
|
||||
id: existing.id
|
||||
},
|
||||
data: {
|
||||
start_time: new_norm_shift!.start_time,
|
||||
end_time: new_norm_shift!.end_time,
|
||||
is_remote: new_norm_shift!.is_remote,
|
||||
comment: new_norm_shift!.comment ?? null,
|
||||
start_time: new_norm_shift.start_time,
|
||||
end_time: new_norm_shift.end_time,
|
||||
is_remote: new_norm_shift.is_remote,
|
||||
is_approved: new_norm_shift.is_approved,
|
||||
comment: new_norm_shift.comment ?? '',
|
||||
bank_code_id: new_bank_code_id,
|
||||
},
|
||||
});
|
||||
action = 'updated';
|
||||
} else throw new BadRequestException('At least one of old_shift or new_shift must be provided');
|
||||
await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only,'create');
|
||||
const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_iso);
|
||||
return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)};
|
||||
});
|
||||
}
|
||||
|
||||
//switches regular hours to overtime hours when exceeds 40hrs per week.
|
||||
await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx);
|
||||
//_________________________________________________________________
|
||||
// UPDATE
|
||||
//_________________________________________________________________
|
||||
private async updateShift(
|
||||
employee_id: number,
|
||||
date_iso: string,
|
||||
dto: UpsertShiftDto,
|
||||
): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const date_only = toDateOnly(date_iso);
|
||||
const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only);
|
||||
|
||||
//Reload the day (truth source)
|
||||
const fresh_day = await tx.shifts.findMany({
|
||||
where: {
|
||||
date: date_only,
|
||||
timesheet_id: timesheet.id,
|
||||
const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift');
|
||||
const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift');
|
||||
|
||||
const old_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, old_norm_shift.type, 'old_shift');
|
||||
const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift');
|
||||
|
||||
const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_iso);
|
||||
const existing = await this.helpersService.findExactOldShift(tx, {
|
||||
timesheet_id,
|
||||
date_only,
|
||||
norm: old_norm_shift,
|
||||
bank_code_id: old_bank_code_id,
|
||||
});
|
||||
if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else');
|
||||
|
||||
await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id);
|
||||
|
||||
await tx.shifts.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
start_time: new_norm_shift.start_time,
|
||||
end_time: new_norm_shift.end_time,
|
||||
is_remote: new_norm_shift.is_remote,
|
||||
comment: new_norm_shift.comment ?? '',
|
||||
bank_code_id: new_bank_code_id,
|
||||
},
|
||||
include: { bank_code: true },
|
||||
orderBy: { start_time: 'asc' },
|
||||
});
|
||||
await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only, 'update');
|
||||
const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_iso);
|
||||
return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)};
|
||||
});
|
||||
|
||||
try {
|
||||
const [ daily_overtime, weekly_overtime ] = await Promise.all([
|
||||
this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only),
|
||||
this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only),
|
||||
]);
|
||||
this.logger.debug(`[OVERTIME] employee_id= ${employee_id}, date=${date_only.toISOString().slice(0,10)}
|
||||
| daily= ${daily_overtime.toFixed(2)}h, weekly: ${weekly_overtime.toFixed(2)}h, (action:${action})`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to compute overtime after ${action} : ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
action,
|
||||
day: fresh_day.map<DayShiftResponse>((shift) => ({
|
||||
start_time: formatHHmm(shift.start_time),
|
||||
end_time: formatHHmm(shift.end_time),
|
||||
type: shift.bank_code?.type ?? 'UNKNOWN',
|
||||
is_remote: shift.is_remote,
|
||||
comment: shift.comment ?? null,
|
||||
})),
|
||||
};
|
||||
//_________________________________________________________________
|
||||
// DELETE
|
||||
//_________________________________________________________________
|
||||
private async deleteShift(
|
||||
employee_id: number,
|
||||
date_iso: string,
|
||||
dto: UpsertShiftDto,
|
||||
): Promise<{ action: UpsertAction; day: DayShiftResponse[]; }>{
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const date_only = toDateOnly(date_iso);
|
||||
const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only);
|
||||
|
||||
const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift');
|
||||
const old_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, old_norm_shift.type, 'old_shift');
|
||||
|
||||
const existing = await this.helpersService.findExactOldShift(tx, {
|
||||
timesheet_id,
|
||||
date_only,
|
||||
norm: old_norm_shift,
|
||||
bank_code_id: old_bank_code_id,
|
||||
});
|
||||
if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else');
|
||||
|
||||
await tx.shifts.delete({ where: { id: existing.id } });
|
||||
|
||||
await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only, 'delete');
|
||||
const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_iso);
|
||||
return { action: 'delete', day: await this.helpersService.mapDay(fresh_shift)};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,20 @@ import { NotificationsModule } from '../notifications/notifications.module';
|
|||
import { ShiftsQueryService } from './services/shifts-query.service';
|
||||
import { ShiftsArchivalService } from './services/shifts-archival.service';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ShiftsHelpersService } from './helpers/shifts.helpers';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BusinessLogicsModule,
|
||||
NotificationsModule,
|
||||
SharedModule
|
||||
SharedModule,
|
||||
],
|
||||
controllers: [ShiftsController],
|
||||
providers: [
|
||||
ShiftsQueryService,
|
||||
ShiftsCommandService,
|
||||
ShiftsArchivalService,
|
||||
ShiftsHelpersService,
|
||||
],
|
||||
exports: [
|
||||
ShiftsQueryService,
|
||||
|
|
|
|||
|
|
@ -5,5 +5,3 @@ export type DayShiftResponse = {
|
|||
is_remote: boolean;
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||
|
|
@ -8,6 +8,7 @@ import { BusinessLogicsModule } from 'src/modules/business-logics/business-l
|
|||
import { SharedModule } from '../shared/shared.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors';
|
||||
import { ShiftsHelpersService } from '../shifts/helpers/shifts.helpers';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -22,6 +23,7 @@ import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.sele
|
|||
ExpensesCommandService,
|
||||
TimesheetArchiveService,
|
||||
TimesheetSelectorsService,
|
||||
ShiftsHelpersService,
|
||||
],
|
||||
exports: [
|
||||
TimesheetsQueryService,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user