From cb6ec29992b0a989d6b06ee265d7f2ccc2460aa2 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 6 Aug 2025 13:15:34 -0400 Subject: [PATCH] refactor(validation): Partial modification of the file structure for validation process, added migration 20250806 --- .../migration.sql | 2 + prisma/schema.prisma | 4 +- src/app.module.ts | 2 - .../services/expenses-approval.service.ts | 11 ++ .../services/pay-periods-approval.service.ts | 5 + .../shifts-overview.controller.ts} | 12 +-- .../shifts/controllers/shifts.controller.ts | 102 ++++++++++-------- .../get-shifts-overview.dto.ts} | 2 +- .../services/shifts-approval.service.ts | 21 ++++ .../shifts-overview.service.ts} | 9 +- src/modules/shifts/shifts.module.ts | 14 ++- .../validation/shifts-validation.module.ts | 11 -- .../services/timesheets-approval.service.ts | 19 ++++ 13 files changed, 135 insertions(+), 79 deletions(-) create mode 100644 prisma/migrations/20250806151558_adding_validation_status_to_shifts_enum_wedding_added/migration.sql create mode 100644 src/modules/expenses/services/expenses-approval.service.ts create mode 100644 src/modules/pay-periods/services/pay-periods-approval.service.ts rename src/modules/shifts/{validation/controllers/shifts-validation.controller.ts => controllers/shifts-overview.controller.ts} (78%) rename src/modules/shifts/{validation/dtos/get-shifts-validation.dto.ts => dtos/get-shifts-overview.dto.ts} (82%) create mode 100644 src/modules/shifts/services/shifts-approval.service.ts rename src/modules/shifts/{validation/services/shifts-validation.service.ts => services/shifts-overview.service.ts} (95%) delete mode 100644 src/modules/shifts/validation/shifts-validation.module.ts create mode 100644 src/modules/timesheets/services/timesheets-approval.service.ts diff --git a/prisma/migrations/20250806151558_adding_validation_status_to_shifts_enum_wedding_added/migration.sql b/prisma/migrations/20250806151558_adding_validation_status_to_shifts_enum_wedding_added/migration.sql new file mode 100644 index 0000000..6944f34 --- /dev/null +++ b/prisma/migrations/20250806151558_adding_validation_status_to_shifts_enum_wedding_added/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "shifts" ADD COLUMN "is_approved" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3d317ee..7adc6f3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -175,6 +175,7 @@ model Shifts { date DateTime @db.Date start_time DateTime @db.Time(0) end_time DateTime @db.Time(0) + is_approved Boolean @default(false) archive ShiftsArchive[] @relation("ShiftsToArchive") @@ -283,7 +284,8 @@ enum LeaveTypes { BEREAVEMENT // deuil de famille PARENTAL // maternite/paternite/adoption LEGAL // obligations legales comme devoir de juree - + WEDDING // mariage + @@map("leave_types") } diff --git a/src/app.module.ts b/src/app.module.ts index c22aaab..2e3db65 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,7 +18,6 @@ import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; -import { ShiftsValidationModule } from './modules/shifts/validation/shifts-validation.module'; import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; @Module({ @@ -37,7 +36,6 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod PayperiodsModule, PrismaModule, ShiftsModule, - ShiftsValidationModule, TimesheetsModule, UsersModule, ], diff --git a/src/modules/expenses/services/expenses-approval.service.ts b/src/modules/expenses/services/expenses-approval.service.ts new file mode 100644 index 0000000..5d5c49f --- /dev/null +++ b/src/modules/expenses/services/expenses-approval.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class ExpensesApprovalService { + constructor(private readonly prisma: PrismaService) {} + + async updateApproval(expenseId: number, isApproved: boolean) { + + } +} \ 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 new file mode 100644 index 0000000..c80779e --- /dev/null +++ b/src/modules/pay-periods/services/pay-periods-approval.service.ts @@ -0,0 +1,5 @@ +import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service"; + +export class PayPeriodsApprovalService { + constructor(private readonly timesheetsApproval: TimesheetsApprovalService) {} +} \ No newline at end of file diff --git a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts b/src/modules/shifts/controllers/shifts-overview.controller.ts similarity index 78% rename from src/modules/shifts/validation/controllers/shifts-validation.controller.ts rename to src/modules/shifts/controllers/shifts-overview.controller.ts index 298808f..3958286 100644 --- a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts +++ b/src/modules/shifts/controllers/shifts-overview.controller.ts @@ -1,20 +1,20 @@ import { Controller, Get, Header, Query } from "@nestjs/common"; -import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service"; -import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto"; +import { OverviewRow, ShiftsOverviewService } from "../services/shifts-overview.service"; +import { GetShiftsOverviewDto } from "../dtos/get-shifts-overview.dto"; @Controller() -export class ShiftsValidationController { - constructor(private readonly shiftsValidationService: ShiftsValidationService) {} +export class ShiftsOverviewController { + constructor(private readonly shiftsValidationService: ShiftsOverviewService) {} @Get() - async getSummary( @Query() query: GetShiftsValidationDto): Promise { + async getSummary( @Query() query: GetShiftsOverviewDto): Promise { return this.shiftsValidationService.getSummary(query.periodId); } @Get('export.csv') @Header('Content-Type', 'text/csv; charset=utf-8') @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') - async exportCsv(@Query() query: GetShiftsValidationDto): Promise{ + async exportCsv(@Query() query: GetShiftsOverviewDto): Promise{ const rows = await this.shiftsValidationService.getSummary(query.periodId); //CSV Headers diff --git a/src/modules/shifts/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts index c7ed0ca..981eb00 100644 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ b/src/modules/shifts/controllers/shifts.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 { ShiftsService } from "../services/shifts.service"; import { Shifts } from "@prisma/client"; import { CreateShiftDto } from "../dtos/create-shifts.dto"; @@ -8,57 +8,67 @@ 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 { ShiftEntity } from "../dtos/swagger-entities/shift.entity"; +import { ShiftsApprovalService } from "../services/shifts-approval.service"; @ApiTags('Shifts') @ApiBearerAuth('access-token') @UseGuards(JwtAuthGuard) @Controller('shifts') export class ShiftsController { - constructor(private readonly shiftsService: ShiftsService){} - - @Post() - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Create shift' }) - @ApiResponse({ status: 201, description: 'Shift created',type: ShiftEntity }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body() dto: CreateShiftDto): Promise { - return this.shiftsService.create(dto); - } + constructor( + private readonly shiftsService: ShiftsService, + private readonly shiftsApprovalService: ShiftsApprovalService, + ){} - @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: ShiftEntity, isArray: true }) - @ApiResponse({ status: 400, description: 'List of shifts not found' }) - findAll(): Promise { - return this.shiftsService.findAll(); - } - - @Get(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Find shift' }) - @ApiResponse({ status: 201, description: 'Shift found',type: ShiftEntity }) - @ApiResponse({ status: 400, description: 'Shift not found' }) - findOne(@Param('id', ParseIntPipe) id: number): Promise { - 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: ShiftEntity }) - @ApiResponse({ status: 400, description: 'Shift not found' }) - update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise { - 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: ShiftEntity }) - @ApiResponse({ status: 400, description: 'Shift not found' }) - remove(@Param('id', ParseIntPipe) id: number): Promise { - return this.shiftsService.remove(id); - } + @Post() + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Create shift' }) + @ApiResponse({ status: 201, description: 'Shift created',type: ShiftEntity }) + @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) + create(@Body() dto: CreateShiftDto): Promise { + 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: ShiftEntity, isArray: true }) + @ApiResponse({ status: 400, description: 'List of shifts not found' }) + findAll(): Promise { + return this.shiftsService.findAll(); + } + + @Get(':id') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Find shift' }) + @ApiResponse({ status: 201, description: 'Shift found',type: ShiftEntity }) + @ApiResponse({ status: 400, description: 'Shift not found' }) + findOne(@Param('id', ParseIntPipe) id: number): Promise { + 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: ShiftEntity }) + @ApiResponse({ status: 400, description: 'Shift not found' }) + update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise { + 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: ShiftEntity }) + @ApiResponse({ status: 400, description: 'Shift not found' }) + remove(@Param('id', ParseIntPipe) id: number): Promise { + return this.shiftsService.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.shiftsApprovalService.updateApproval(id, isApproved); + } } \ No newline at end of file diff --git a/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts b/src/modules/shifts/dtos/get-shifts-overview.dto.ts similarity index 82% rename from src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts rename to src/modules/shifts/dtos/get-shifts-overview.dto.ts index 44b656b..8d2dfc0 100644 --- a/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts +++ b/src/modules/shifts/dtos/get-shifts-overview.dto.ts @@ -1,7 +1,7 @@ import { Type } from "class-transformer"; import { IsInt, Min, Max } from "class-validator"; -export class GetShiftsValidationDto { +export class GetShiftsOverviewDto { @Type(()=> Number) @IsInt() @Min(1) diff --git a/src/modules/shifts/services/shifts-approval.service.ts b/src/modules/shifts/services/shifts-approval.service.ts new file mode 100644 index 0000000..7c0c23b --- /dev/null +++ b/src/modules/shifts/services/shifts-approval.service.ts @@ -0,0 +1,21 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Shifts } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class ShiftsApprovalService { + constructor(private readonly prisma: PrismaService) {} + + 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; + } +} \ No newline at end of file diff --git a/src/modules/shifts/validation/services/shifts-validation.service.ts b/src/modules/shifts/services/shifts-overview.service.ts similarity index 95% rename from src/modules/shifts/validation/services/shifts-validation.service.ts rename to src/modules/shifts/services/shifts-overview.service.ts index 5d05f40..c4797fc 100644 --- a/src/modules/shifts/validation/services/shifts-validation.service.ts +++ b/src/modules/shifts/services/shifts-overview.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; -export interface ValidationRow { +export interface OverviewRow { fullName: string; supervisor: string; totalRegularHrs: number; @@ -13,7 +13,7 @@ export interface ValidationRow { } @Injectable() -export class ShiftsValidationService { +export class ShiftsOverviewService { constructor(private readonly prisma: PrismaService) {} private computeHours(start: Date, end: Date): number { @@ -22,7 +22,7 @@ export class ShiftsValidationService { return parseFloat(hours.toFixed(2)); } - 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 }, @@ -57,7 +57,7 @@ export class ShiftsValidationService { }, }); - const mapRow = new Map(); + const mapRow = new Map(); for(const s of shifts) { const employeeId = s.timesheet.employee.user_id; @@ -119,4 +119,5 @@ export class ShiftsValidationService { //return by default the list of employee in ascending alphabetical order return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); } + } diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index 57cd331..a2b2101 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -2,15 +2,13 @@ import { Module } from '@nestjs/common'; import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsService } from './services/shifts.service'; import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { ShiftsValidationModule } from './validation/shifts-validation.module'; +import { ShiftsOverviewController } from './controllers/shifts-overview.controller'; +import { ShiftsOverviewService } from './services/shifts-overview.service'; @Module({ - imports: [ - BusinessLogicsModule, - ShiftsValidationModule, - ], - controllers: [ShiftsController], - providers: [ShiftsService], - exports: [ShiftsService], + imports: [BusinessLogicsModule], + controllers: [ShiftsController, ShiftsOverviewController], + providers: [ShiftsService, ShiftsOverviewService], + exports: [ShiftsService, ShiftsOverviewService], }) export class ShiftsModule {} diff --git a/src/modules/shifts/validation/shifts-validation.module.ts b/src/modules/shifts/validation/shifts-validation.module.ts deleted file mode 100644 index 277d2ac..0000000 --- a/src/modules/shifts/validation/shifts-validation.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { ShiftsValidationController } from "./controllers/shifts-validation.controller"; -import { ShiftsValidationService } from "./services/shifts-validation.service"; -import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; - -@Module({ - imports: [BusinessLogicsModule], - controllers: [ShiftsValidationController], - providers: [ShiftsValidationService], -}) -export class ShiftsValidationModule {} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-approval.service.ts b/src/modules/timesheets/services/timesheets-approval.service.ts new file mode 100644 index 0000000..81bb35d --- /dev/null +++ b/src/modules/timesheets/services/timesheets-approval.service.ts @@ -0,0 +1,19 @@ + +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Timesheets } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class TimesheetsApprovalService { + constructor(private readonly prisma: PrismaService) {} + + async updateApproval(timesheetId: number, isApproved: boolean): Promise { + const timesheet = await this.prisma.timesheets.update({ + where: { id: timesheetId }, + data: { is_approved: isApproved}, + }); + if (!timesheet) throw new NotFoundException(`Timesheet # ${timesheetId} not found`); + + return timesheet; + } +} \ No newline at end of file