refactor(validation): Partial modification of the file structure for validation process, added migration 20250806

This commit is contained in:
Matthieu Haineault 2025-08-06 13:15:34 -04:00
parent b0406b3a4c
commit cb6ec29992
13 changed files with 135 additions and 79 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "shifts" ADD COLUMN "is_approved" BOOLEAN NOT NULL DEFAULT false;

View File

@ -175,6 +175,7 @@ model Shifts {
date DateTime @db.Date date DateTime @db.Date
start_time DateTime @db.Time(0) start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0) end_time DateTime @db.Time(0)
is_approved Boolean @default(false)
archive ShiftsArchive[] @relation("ShiftsToArchive") archive ShiftsArchive[] @relation("ShiftsToArchive")
@ -283,7 +284,8 @@ enum LeaveTypes {
BEREAVEMENT // deuil de famille BEREAVEMENT // deuil de famille
PARENTAL // maternite/paternite/adoption PARENTAL // maternite/paternite/adoption
LEGAL // obligations legales comme devoir de juree LEGAL // obligations legales comme devoir de juree
WEDDING // mariage
@@map("leave_types") @@map("leave_types")
} }

View File

@ -18,7 +18,6 @@ import { ArchivalModule } from './modules/archival/archival.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; 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'; import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
@Module({ @Module({
@ -37,7 +36,6 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod
PayperiodsModule, PayperiodsModule,
PrismaModule, PrismaModule,
ShiftsModule, ShiftsModule,
ShiftsValidationModule,
TimesheetsModule, TimesheetsModule,
UsersModule, UsersModule,
], ],

View File

@ -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) {
}
}

View File

@ -0,0 +1,5 @@
import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service";
export class PayPeriodsApprovalService {
constructor(private readonly timesheetsApproval: TimesheetsApprovalService) {}
}

View File

@ -1,20 +1,20 @@
import { Controller, Get, Header, Query } from "@nestjs/common"; import { Controller, Get, Header, Query } from "@nestjs/common";
import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service"; import { OverviewRow, ShiftsOverviewService } from "../services/shifts-overview.service";
import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto"; import { GetShiftsOverviewDto } from "../dtos/get-shifts-overview.dto";
@Controller() @Controller()
export class ShiftsValidationController { export class ShiftsOverviewController {
constructor(private readonly shiftsValidationService: ShiftsValidationService) {} constructor(private readonly shiftsValidationService: ShiftsOverviewService) {}
@Get() @Get()
async getSummary( @Query() query: GetShiftsValidationDto): Promise<ValidationRow[]> { async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
return this.shiftsValidationService.getSummary(query.periodId); return this.shiftsValidationService.getSummary(query.periodId);
} }
@Get('export.csv') @Get('export.csv')
@Header('Content-Type', 'text/csv; charset=utf-8') @Header('Content-Type', 'text/csv; charset=utf-8')
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
async exportCsv(@Query() query: GetShiftsValidationDto): Promise<Buffer>{ async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
const rows = await this.shiftsValidationService.getSummary(query.periodId); const rows = await this.shiftsValidationService.getSummary(query.periodId);
//CSV Headers //CSV Headers

View File

@ -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 { ShiftsService } from "../services/shifts.service";
import { Shifts } from "@prisma/client"; import { Shifts } from "@prisma/client";
import { CreateShiftDto } from "../dtos/create-shifts.dto"; 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 { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ShiftEntity } from "../dtos/swagger-entities/shift.entity"; import { ShiftEntity } from "../dtos/swagger-entities/shift.entity";
import { ShiftsApprovalService } from "../services/shifts-approval.service";
@ApiTags('Shifts') @ApiTags('Shifts')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('shifts') @Controller('shifts')
export class ShiftsController { export class ShiftsController {
constructor(private readonly shiftsService: ShiftsService){} constructor(
private readonly shiftsService: ShiftsService,
@Post() private readonly shiftsApprovalService: ShiftsApprovalService,
@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<Shifts> {
return this.shiftsService.create(dto);
}
@Get() @Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Find all shifts' }) @ApiOperation({ summary: 'Create shift' })
@ApiResponse({ status: 201, description: 'List of shifts found',type: ShiftEntity, isArray: true }) @ApiResponse({ status: 201, description: 'Shift created',type: ShiftEntity })
@ApiResponse({ status: 400, description: 'List of shifts not found' }) @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
findAll(): Promise<Shifts[]> { create(@Body() dto: CreateShiftDto): Promise<Shifts> {
return this.shiftsService.findAll(); return this.shiftsService.create(dto);
} }
@Get(':id') @Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Find shift' }) @ApiOperation({ summary: 'Find all shifts' })
@ApiResponse({ status: 201, description: 'Shift found',type: ShiftEntity }) @ApiResponse({ status: 201, description: 'List of shifts found',type: ShiftEntity, isArray: true })
@ApiResponse({ status: 400, description: 'Shift not found' }) @ApiResponse({ status: 400, description: 'List of shifts not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> { findAll(): Promise<Shifts[]> {
return this.shiftsService.findOne(id); return this.shiftsService.findAll();
} }
@Patch(':id') @Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Update shift' }) @ApiOperation({ summary: 'Find shift' })
@ApiResponse({ status: 201, description: 'Shift updated',type: ShiftEntity }) @ApiResponse({ status: 201, description: 'Shift found',type: ShiftEntity })
@ApiResponse({ status: 400, description: 'Shift not found' }) @ApiResponse({ status: 400, description: 'Shift not found' })
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise<Shifts> { findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
return this.shiftsService.update(id, dto); return this.shiftsService.findOne(id);
} }
@Delete(':id') @Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Delete shift' }) @ApiOperation({ summary: 'Update shift' })
@ApiResponse({ status: 201, description: 'Shift deleted',type: ShiftEntity }) @ApiResponse({ status: 201, description: 'Shift updated',type: ShiftEntity })
@ApiResponse({ status: 400, description: 'Shift not found' }) @ApiResponse({ status: 400, description: 'Shift not found' })
remove(@Param('id', ParseIntPipe) id: number): Promise<Shifts> { update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise<Shifts> {
return this.shiftsService.remove(id); 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<Shifts> {
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);
}
} }

View File

@ -1,7 +1,7 @@
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { IsInt, Min, Max } from "class-validator"; import { IsInt, Min, Max } from "class-validator";
export class GetShiftsValidationDto { export class GetShiftsOverviewDto {
@Type(()=> Number) @Type(()=> Number)
@IsInt() @IsInt()
@Min(1) @Min(1)

View File

@ -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<Shifts> {
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;
}
}

View File

@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
export interface ValidationRow { export interface OverviewRow {
fullName: string; fullName: string;
supervisor: string; supervisor: string;
totalRegularHrs: number; totalRegularHrs: number;
@ -13,7 +13,7 @@ export interface ValidationRow {
} }
@Injectable() @Injectable()
export class ShiftsValidationService { export class ShiftsOverviewService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
private computeHours(start: Date, end: Date): number { private computeHours(start: Date, end: Date): number {
@ -22,7 +22,7 @@ export class ShiftsValidationService {
return parseFloat(hours.toFixed(2)); return parseFloat(hours.toFixed(2));
} }
async getSummary(periodId: number): Promise<ValidationRow[]> { async getSummary(periodId: number): Promise<OverviewRow[]> {
//fetch pay-period to display //fetch pay-period to display
const period = await this.prisma.payPeriods.findUnique({ const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodId }, where: { period_number: periodId },
@ -57,7 +57,7 @@ export class ShiftsValidationService {
}, },
}); });
const mapRow = new Map<string, ValidationRow>(); const mapRow = new Map<string, OverviewRow>();
for(const s of shifts) { for(const s of shifts) {
const employeeId = s.timesheet.employee.user_id; 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 by default the list of employee in ascending alphabetical order
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
} }
} }

View File

@ -2,15 +2,13 @@ import { Module } from '@nestjs/common';
import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsController } from './controllers/shifts.controller';
import { ShiftsService } from './services/shifts.service'; import { ShiftsService } from './services/shifts.service';
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; 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({ @Module({
imports: [ imports: [BusinessLogicsModule],
BusinessLogicsModule, controllers: [ShiftsController, ShiftsOverviewController],
ShiftsValidationModule, providers: [ShiftsService, ShiftsOverviewService],
], exports: [ShiftsService, ShiftsOverviewService],
controllers: [ShiftsController],
providers: [ShiftsService],
exports: [ShiftsService],
}) })
export class ShiftsModule {} export class ShiftsModule {}

View File

@ -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 {}

View File

@ -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<Timesheets> {
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;
}
}