From c23da925e7cba8ce4f5bd3376b6549d8791636cb Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 6 Aug 2025 14:17:52 -0400 Subject: [PATCH] feat(approval): clean up Approval services. creation of a "shared" folder --- src/common/shared/base-approval.service.ts | 26 +++++++++++++ .../controllers/expenses.controller.ts | 14 ++++++- .../services/expenses-approval.service.ts | 12 +++--- .../services/pay-periods-approval.service.ts | 37 ++++++++++++++++++- .../services/pay-periods.service.ts | 18 ++++++++- .../services/shifts-approval.service.ts | 20 +++------- .../services/shifts-overview.service.ts | 2 +- .../controllers/timesheets.controller.ts | 14 ++++++- .../services/timesheets-approval.service.ts | 30 ++++++++++++--- 9 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 src/common/shared/base-approval.service.ts diff --git a/src/common/shared/base-approval.service.ts b/src/common/shared/base-approval.service.ts new file mode 100644 index 0000000..d3e3931 --- /dev/null +++ b/src/common/shared/base-approval.service.ts @@ -0,0 +1,26 @@ +import { NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +//abstract class for approving or rejecting a shift, expense, timesheet or pay-period +export abstract class BaseApprovalService { + protected constructor(protected readonly prisma: PrismaService) {} + + //returns the corresponding Prisma delegate + protected abstract get delegate(): { + update(args: {where: {id: number }; + data: { is_approved: boolean } + }): Promise; + }; + + //standard update Aproval + async updateApproval(id: number, isApproved: boolean): Promise { + const entity = await this.delegate.update({ + where: { id }, + data: { is_approved: isApproved }, + }); + + if(!entity) throw new NotFoundException(`Entity #${id} not found`); + + return entity; + } +} \ No newline at end of file diff --git a/src/modules/expenses/controllers/expenses.controller.ts b/src/modules/expenses/controllers/expenses.controller.ts index 91e5b93..36e64c5 100644 --- a/src/modules/expenses/controllers/expenses.controller.ts +++ b/src/modules/expenses/controllers/expenses.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; import { ExpensesService } from "../services/expenses.service"; import { CreateExpenseDto } from "../dtos/create-expense"; import { Expenses } from "@prisma/client"; @@ -8,13 +8,17 @@ import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagg import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; import { ExpenseEntity } from "../dtos/swagger-entities/expenses.entity"; +import { ExpensesApprovalService } from "../services/expenses-approval.service"; @ApiTags('Expenses') @ApiBearerAuth('access-token') @UseGuards(JwtAuthGuard) @Controller('Expenses') export class ExpensesController { - constructor(private readonly expensesService: ExpensesService) {} + constructor( + private readonly expensesService: ExpensesService, + private readonly expensesApprovalService: ExpensesApprovalService, + ) {} @Post() @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @@ -61,4 +65,10 @@ export class ExpensesController { return this.expensesService.remove(id); } + @Patch(':id/approval') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { + return this.expensesApprovalService.updateApproval(id, isApproved); + } + } \ No newline at end of file diff --git a/src/modules/expenses/services/expenses-approval.service.ts b/src/modules/expenses/services/expenses-approval.service.ts index 5d5c49f..412cf1c 100644 --- a/src/modules/expenses/services/expenses-approval.service.ts +++ b/src/modules/expenses/services/expenses-approval.service.ts @@ -1,11 +1,13 @@ import { Injectable } from "@nestjs/common"; +import { Expenses } from "@prisma/client"; +import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() -export class ExpensesApprovalService { - constructor(private readonly prisma: PrismaService) {} +export class ExpensesApprovalService extends BaseApprovalService { + constructor(prisma: PrismaService) { super(prisma); } - async updateApproval(expenseId: number, isApproved: boolean) { - - } + protected get delegate() { + return this.prisma.expenses; + } } \ No newline at end of file diff --git a/src/modules/pay-periods/services/pay-periods-approval.service.ts b/src/modules/pay-periods/services/pay-periods-approval.service.ts index c80779e..66d080b 100644 --- a/src/modules/pay-periods/services/pay-periods-approval.service.ts +++ b/src/modules/pay-periods/services/pay-periods-approval.service.ts @@ -1,5 +1,40 @@ +import { NotFoundException } from "@nestjs/common"; import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service"; +import { PrismaService } from "src/prisma/prisma.service"; export class PayPeriodsApprovalService { - constructor(private readonly timesheetsApproval: TimesheetsApprovalService) {} + constructor( + private readonly prisma: PrismaService, + private readonly timesheetsApproval: TimesheetsApprovalService, + ) {} + + async approvaPayperdiod(periodNumber: number): Promise { + const period = await this.prisma.payPeriods.findUnique({ + where: { period_number: periodNumber }, + }); + if (!period) throw new NotFoundException(`PayPeriod #${periodNumber} not found`); + + //fetches timesheet of selected period if the timesheet as atleast 1 shift or 1 expense + const timesheetList = await this.prisma.timesheets.findMany({ + where: { + OR: [ + { shift: {some: { date: { gte: period.start_date, + lte: period.end_date, + }, + }}, + }, + { expense: { some: { date: { gte: period.start_date, + lte: period.end_date, + }, + }}, + }, + ], + }, + }); + + //approval of both timesheet (cascading to the approval of related shifts and expenses) + for(const timesheet of timesheetList) { + await this.timesheetsApproval.updateApproval(timesheet.id, true); + } + } } \ No newline at end of file diff --git a/src/modules/pay-periods/services/pay-periods.service.ts b/src/modules/pay-periods/services/pay-periods.service.ts index 0202774..cca3068 100644 --- a/src/modules/pay-periods/services/pay-periods.service.ts +++ b/src/modules/pay-periods/services/pay-periods.service.ts @@ -1,10 +1,15 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, NotFoundException, Param, ParseIntPipe, Patch } from "@nestjs/common"; import { PayPeriods } from "@prisma/client"; import { PrismaService } from "src/prisma/prisma.service"; +import { PayPeriodsApprovalService } from "./pay-periods-approval.service"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { Roles as RoleEnum } from '.prisma/client'; @Injectable() export class PayPeriodsService { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService, + private readonly payperiodsApprovalService: PayPeriodsApprovalService + ) {} async findAll(): Promise { return this.prisma.payPeriods.findMany({ @@ -32,4 +37,13 @@ export class PayPeriodsService { } return period; } + + @Patch(':periodNumber/approval') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async approve(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<{message:string}> { + await this.payperiodsApprovalService.approvaPayperdiod(periodNumber); + return {message: `Pay-period #${periodNumber} approved`}; + } + + } \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-approval.service.ts b/src/modules/shifts/services/shifts-approval.service.ts index 7c0c23b..5e07e5b 100644 --- a/src/modules/shifts/services/shifts-approval.service.ts +++ b/src/modules/shifts/services/shifts-approval.service.ts @@ -1,21 +1,13 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Shifts } from "@prisma/client"; +import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() -export class ShiftsApprovalService { - constructor(private readonly prisma: PrismaService) {} +export class ShiftsApprovalService extends BaseApprovalService { + constructor(prisma: PrismaService) { super(prisma); } - async updateApproval(shiftId: number, isApproved: boolean): Promise { - const shift = await this.prisma.shifts.update({ - where: { id: shiftId }, - data: { is_approved: isApproved }, - }); - - if(!shift) { - throw new NotFoundException(`Shift # ${shiftId} not found`); - } - - return shift; - } + protected get delegate() { + return this.prisma.shifts; + } } \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-overview.service.ts b/src/modules/shifts/services/shifts-overview.service.ts index c080c24..d5544dd 100644 --- a/src/modules/shifts/services/shifts-overview.service.ts +++ b/src/modules/shifts/services/shifts-overview.service.ts @@ -17,7 +17,7 @@ export interface OverviewRow { export class ShiftsOverviewService { constructor(private readonly prisma: PrismaService) {} - async getSummary(periodId: number): Promise { + async getSummary(periodId: number): Promise { //fetch pay-period to display const period = await this.prisma.payPeriods.findUnique({ where: { period_number: periodId }, diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index e90e08f..504d66b 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common'; import { TimesheetsService } from '../services/timesheets.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets } from '@prisma/client'; @@ -8,13 +8,17 @@ import { Roles as RoleEnum } from '.prisma/client'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard'; import { TimesheetEntity } from '../dtos/swagger-entities/timesheet.entity'; +import { TimesheetsApprovalService } from '../services/timesheets-approval.service'; @ApiTags('Timesheets') @ApiBearerAuth('access-token') @UseGuards(JwtAuthGuard) @Controller('timesheets') export class TimesheetsController { - constructor(private readonly timesheetsService: TimesheetsService) {} + constructor( + private readonly timesheetsService: TimesheetsService, + private readonly timesheetsApprovalService: TimesheetsApprovalService, + ) {} @Post() @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @@ -63,4 +67,10 @@ export class TimesheetsController { remove(@Param('id', ParseIntPipe) id: number): Promise { return this.timesheetsService.remove(id); } + + @Patch(':id/approval') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { + return this.timesheetsApprovalService.updateApproval(id, isApproved); + } } diff --git a/src/modules/timesheets/services/timesheets-approval.service.ts b/src/modules/timesheets/services/timesheets-approval.service.ts index 81bb35d..880e035 100644 --- a/src/modules/timesheets/services/timesheets-approval.service.ts +++ b/src/modules/timesheets/services/timesheets-approval.service.ts @@ -1,19 +1,37 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Timesheets } from "@prisma/client"; +import { BaseApprovalService } from "src/common/shared/base-approval.service"; +import { ExpensesApprovalService } from "src/modules/expenses/services/expenses-approval.service"; +import { ShiftsApprovalService } from "src/modules/shifts/services/shifts-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() -export class TimesheetsApprovalService { - constructor(private readonly prisma: PrismaService) {} +export class TimesheetsApprovalService extends BaseApprovalService{ + constructor( + prisma: PrismaService, + private readonly shiftsApproval: ShiftsApprovalService, + private readonly expensesApproval: ExpensesApprovalService, + ) {super(prisma);} + + protected get delegate() { + return this.prisma.timesheets; + } async updateApproval(timesheetId: number, isApproved: boolean): Promise { - const timesheet = await this.prisma.timesheets.update({ - where: { id: timesheetId }, - data: { is_approved: isApproved}, + const timesheet = await super.updateApproval(timesheetId, isApproved); + + await this.prisma.shifts.updateMany({ + where: { timesheet_id: timesheetId }, + data: { is_approved: isApproved }, + }); + + await this.prisma.expenses.updateMany({ + where: { timesheet_id: timesheetId }, + data: { is_approved: isApproved }, }); - if (!timesheet) throw new NotFoundException(`Timesheet # ${timesheetId} not found`); return timesheet; } + } \ No newline at end of file