From 02ebb23d7a006661e584778ef746fbfa540c1a96 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 5 Nov 2025 08:36:24 -0500 Subject: [PATCH 1/2] refactor(employees): uncomment module and comment archival parts(needs refactoring) --- .../controllers/employees.controller.ts | 168 ++++--- .../employees/dtos/create-employee.dto.ts | 208 ++++----- .../employees/dtos/list-employee.dto.ts | 16 +- .../employees/dtos/profil-employee.dto.ts | 26 +- .../employees/dtos/update-employee.dto.ts | 36 +- .../services/employees-archival.service.ts | 308 ++++++------- .../employees/services/employees.service.ts | 418 +++++++++--------- .../employees/utils/employee.utils.ts | 18 +- 8 files changed, 594 insertions(+), 604 deletions(-) diff --git a/src/identity-and-account/employees/controllers/employees.controller.ts b/src/identity-and-account/employees/controllers/employees.controller.ts index 4828d91..dabca96 100644 --- a/src/identity-and-account/employees/controllers/employees.controller.ts +++ b/src/identity-and-account/employees/controllers/employees.controller.ts @@ -1,99 +1,89 @@ -// import { Body,Controller,Get,NotFoundException,Param,Patch } from '@nestjs/common'; -// import { EmployeesService } from '../services/employees.service'; -// import { CreateEmployeeDto } from '../dtos/create-employee.dto'; -// import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; -// import { RolesAllowed } from '../../../common/decorators/roles.decorators'; -// import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -// import { EmployeeListItemDto } from '../dtos/list-employee.dto'; -// import { EmployeesArchivalService } from '../services/employees-archival.service'; -// import { EmployeeProfileItemDto } from 'src/modules/employees/dtos/profil-employee.dto'; +import { Controller, Get, Patch, Param, Body, NotFoundException } from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; +import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto"; +import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto"; +import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto"; +import { EmployeesArchivalService } from "src/identity-and-account/employees/services/employees-archival.service"; +import { EmployeesService } from "src/identity-and-account/employees/services/employees.service"; -// @ApiTags('Employees') -// @ApiBearerAuth('access-token') -// // @UseGuards() -// @Controller('employees') -// export class EmployeesController { -// constructor( -// private readonly employeesService: EmployeesService, -// private readonly archiveService: EmployeesArchivalService, -// ) {} +@ApiBearerAuth('access-token') +// @UseGuards() +@Controller('employees') +export class EmployeesController { + constructor( + private readonly employeesService: EmployeesService, + private readonly archiveService: EmployeesArchivalService, + ) {} -// @Get('employee-list') -// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) -// @ApiOperation({summary: 'Find all employees with scoped info' }) -// @ApiResponse({ status: 200, description: 'List of employees with scoped info found', type: EmployeeListItemDto, isArray: true }) -// @ApiResponse({ status: 400, description: 'List of employees with scoped info not found' }) -// findListEmployees(): Promise { -// return this.employeesService.findListEmployees(); -// } + @Get('employee-list') + //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) -// @Patch(':email') -// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) -// @ApiBearerAuth('access-token') -// @ApiOperation({ summary: 'Update, archive or restore an employee' }) -// @ApiParam({ name: 'email', type: Number, description: 'Email of the employee' }) -// @ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto }) -// @ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto }) -// @ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) -// async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) { -// // if last_work_day is set => archive the employee -// // else if employee is archived and first_work_day or last_work_day = null => restore -// //otherwise => standard update -// const result = await this.archiveService.patchEmployee(email, dto); -// if(!result) { -// throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`) -// } -// return result; -// } + findListEmployees(): Promise { + return this.employeesService.findListEmployees(); + } -// //_____________________________________________________________________________________________ -// // Deprecated or unused methods -// //_____________________________________________________________________________________________ + @Patch(':email') + //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiBearerAuth('access-token') + async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) { + // if last_work_day is set => archive the employee + // else if employee is archived and first_work_day or last_work_day = null => restore + //otherwise => standard update + const result = await this.archiveService.patchEmployee(email, dto); + if(!result) { + throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`) + } + return result; + } -// // @Post() -// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) -// // @ApiOperation({summary: 'Create employee' }) -// // @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto }) -// // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) -// // create(@Body() dto: CreateEmployeeDto): Promise { -// // return this.employeesService.create(dto); -// // } -// // @Get() -// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) -// // @ApiOperation({summary: 'Find all employees' }) -// // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true }) -// // @ApiResponse({ status: 400, description: 'List of employees not found' }) -// // findAll(): Promise { -// // return this.employeesService.findAll(); -// // } + //_____________________________________________________________________________________________ + // Deprecated or unused methods + //_____________________________________________________________________________________________ + + // @Post() + // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + // @ApiOperation({summary: 'Create employee' }) + // @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto }) + // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) + // create(@Body() dto: CreateEmployeeDto): Promise { + // return this.employeesService.create(dto); + // } + // @Get() + // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) + // @ApiOperation({summary: 'Find all employees' }) + // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true }) + // @ApiResponse({ status: 400, description: 'List of employees not found' }) + // findAll(): Promise { + // return this.employeesService.findAll(); + // } -// // @Get(':email') -// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) -// // @ApiOperation({summary: 'Find employee' }) -// // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) -// // @ApiResponse({ status: 400, description: 'Employee not found' }) -// // findOne(@Param('email', ParseIntPipe) email: string): Promise { -// // return this.employeesService.findOne(email); -// // } + // @Get(':email') + // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) + // @ApiOperation({summary: 'Find employee' }) + // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) + // @ApiResponse({ status: 400, description: 'Employee not found' }) + // findOne(@Param('email', ParseIntPipe) email: string): Promise { + // return this.employeesService.findOne(email); + // } -// @Get('profile/:email') -// @ApiOperation({summary: 'Find employee profile' }) -// @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) -// @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) -// @ApiResponse({ status: 400, description: 'Employee profile not found' }) -// findOneProfile(@Param('email') email: string): Promise { -// return this.employeesService.findOneProfile(email); -// } + @Get('profile/:email') + @ApiOperation({summary: 'Find employee profile' }) + @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) + @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) + @ApiResponse({ status: 400, description: 'Employee profile not found' }) + findOneProfile(@Param('email') email: string): Promise { + return this.employeesService.findOneProfile(email); + } -// // @Delete(':email') -// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) -// // @ApiOperation({summary: 'Delete employee' }) -// // @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' }) -// // @ApiResponse({ status: 204, description: 'Employee deleted' }) -// // @ApiResponse({ status: 404, description: 'Employee not found' }) -// // remove(@Param('email', ParseIntPipe) email: string): Promise { -// // return this.employeesService.remove(email); -// // } + // @Delete(':email') + // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) + // @ApiOperation({summary: 'Delete employee' }) + // @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' }) + // @ApiResponse({ status: 204, description: 'Employee deleted' }) + // @ApiResponse({ status: 404, description: 'Employee not found' }) + // remove(@Param('email', ParseIntPipe) email: string): Promise { + // return this.employeesService.remove(email); + // } -// } +} diff --git a/src/identity-and-account/employees/dtos/create-employee.dto.ts b/src/identity-and-account/employees/dtos/create-employee.dto.ts index 4fbbaaa..89279ef 100644 --- a/src/identity-and-account/employees/dtos/create-employee.dto.ts +++ b/src/identity-and-account/employees/dtos/create-employee.dto.ts @@ -1,118 +1,118 @@ -// import { -// Allow, -// IsBoolean, -// IsDateString, -// IsEmail, -// IsInt, -// IsNotEmpty, -// IsOptional, -// IsPositive, -// IsString, -// IsUUID, -// } from 'class-validator'; -// import { Type } from 'class-transformer'; -// import { ApiProperty } from '@nestjs/swagger'; +import { + Allow, + IsBoolean, + IsDateString, + IsEmail, + IsInt, + IsNotEmpty, + IsOptional, + IsPositive, + IsString, + IsUUID, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; -// export class CreateEmployeeDto { -// @ApiProperty({ -// example: 1, -// description: 'Unique ID of an employee(primary-key, auto-incremented)', -// }) -// @Allow() -// id: number; +export class CreateEmployeeDto { + @ApiProperty({ + example: 1, + description: 'Unique ID of an employee(primary-key, auto-incremented)', + }) + @Allow() + id: number; -// @ApiProperty({ -// example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', -// description: 'UUID of the user linked to that employee', -// }) -// @IsUUID() -// @IsOptional() -// user_id?: string; + @ApiProperty({ + example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', + description: 'UUID of the user linked to that employee', + }) + @IsUUID() + @IsOptional() + user_id?: string; -// @ApiProperty({ -// example: 'Frodo', -// description: 'Employee`s first name', -// }) -// @IsString() -// @IsNotEmpty() -// first_name: string; + @ApiProperty({ + example: 'Frodo', + description: 'Employee`s first name', + }) + @IsString() + @IsNotEmpty() + first_name: string; -// @ApiProperty({ -// example: 'Baggins', -// description: 'Employee`s last name', -// }) -// @IsString() -// @IsNotEmpty() -// last_name: string; + @ApiProperty({ + example: 'Baggins', + description: 'Employee`s last name', + }) + @IsString() + @IsNotEmpty() + last_name: string; -// @ApiProperty({ -// example: 'i_cant_do_this_sam@targointernet.com', -// description: 'Employee`s email', -// }) -// @IsEmail() -// @IsOptional() -// email: string; + @ApiProperty({ + example: 'i_cant_do_this_sam@targointernet.com', + description: 'Employee`s email', + }) + @IsEmail() + @IsOptional() + email: string; -// @IsOptional() -// @IsBoolean() -// is_supervisor: boolean; + @IsOptional() + @IsBoolean() + is_supervisor: boolean; -// @ApiProperty({ -// example: '82538437464', -// description: 'Employee`s phone number', -// }) -// @IsString() -// phone_number: string; + @ApiProperty({ + example: '82538437464', + description: 'Employee`s phone number', + }) + @IsString() + phone_number: string; -// @ApiProperty({ -// example: '1 Bagshot Row, Hobbiton, The Shire, Middle-earth', -// description: 'Employee`s residence', -// required: false, -// }) -// @IsString() -// @IsOptional() -// residence?: string; + @ApiProperty({ + example: '1 Bagshot Row, Hobbiton, The Shire, Middle-earth', + description: 'Employee`s residence', + required: false, + }) + @IsString() + @IsOptional() + residence?: string; -// @ApiProperty({ -// example: 7464, -// description: 'external ID for the pay system', -// }) -// @IsInt() -// @IsPositive() -// @Type(() => Number) -// external_payroll_id: number; + @ApiProperty({ + example: 7464, + description: 'external ID for the pay system', + }) + @IsInt() + @IsPositive() + @Type(() => Number) + external_payroll_id: number; -// @ApiProperty({ -// example: 335567447, -// description: 'Employee`s company code', -// }) -// @IsInt() -// @IsPositive() -// @Type(() => Number) -// company_code: number; + @ApiProperty({ + example: 335567447, + description: 'Employee`s company code', + }) + @IsInt() + @IsPositive() + @Type(() => Number) + company_code: number; -// @ApiProperty({ -// example:'technicient', -// description: 'employee`s job title', -// }) -// @IsString() -// @IsOptional() -// job_title: string; + @ApiProperty({ + example:'technicient', + description: 'employee`s job title', + }) + @IsString() + @IsOptional() + job_title: string; -// @ApiProperty({ -// example: '23/09/3018', -// description: 'Employee`s first working day', -// }) -// @IsDateString() -// first_work_day: string; + @ApiProperty({ + example: '23/09/3018', + description: 'Employee`s first working day', + }) + @IsDateString() + first_work_day: string; -// @ApiProperty({ -// example: '25/03/3019', -// description: 'Employee`s last working day', -// required: false, -// }) -// @IsDateString() -// @IsOptional() -// last_work_day?: string; -// } + @ApiProperty({ + example: '25/03/3019', + description: 'Employee`s last working day', + required: false, + }) + @IsDateString() + @IsOptional() + last_work_day?: string; +} diff --git a/src/identity-and-account/employees/dtos/list-employee.dto.ts b/src/identity-and-account/employees/dtos/list-employee.dto.ts index 6adbe3f..39abf03 100644 --- a/src/identity-and-account/employees/dtos/list-employee.dto.ts +++ b/src/identity-and-account/employees/dtos/list-employee.dto.ts @@ -1,8 +1,8 @@ -// export class EmployeeListItemDto { -// first_name: string; -// last_name: string; -// email: string; -// supervisor_full_name: string | null; -// company_name: number | null; -// job_title: string | null; -// } \ No newline at end of file +export class EmployeeListItemDto { + first_name: string; + last_name: string; + email: string; + supervisor_full_name: string | null; + company_name: number | null; + job_title: string | null; +} \ No newline at end of file diff --git a/src/identity-and-account/employees/dtos/profil-employee.dto.ts b/src/identity-and-account/employees/dtos/profil-employee.dto.ts index adbf38e..c6836cf 100644 --- a/src/identity-and-account/employees/dtos/profil-employee.dto.ts +++ b/src/identity-and-account/employees/dtos/profil-employee.dto.ts @@ -1,13 +1,13 @@ -// export class EmployeeProfileItemDto { -// first_name: string; -// last_name: string; -// employee_full_name: string; -// supervisor_full_name: string | null; -// company_name: number | null; -// job_title: string | null; -// email: string | null; -// phone_number: string; -// first_work_day: string; -// last_work_day?: string | null; -// residence: string | null; -// } \ No newline at end of file +export class EmployeeProfileItemDto { + first_name: string; + last_name: string; + employee_full_name: string; + supervisor_full_name: string | null; + company_name: number | null; + job_title: string | null; + email: string | null; + phone_number: string; + first_work_day: string; + last_work_day?: string | null; + residence: string | null; +} \ No newline at end of file diff --git a/src/identity-and-account/employees/dtos/update-employee.dto.ts b/src/identity-and-account/employees/dtos/update-employee.dto.ts index 1efbbfd..334a01a 100644 --- a/src/identity-and-account/employees/dtos/update-employee.dto.ts +++ b/src/identity-and-account/employees/dtos/update-employee.dto.ts @@ -1,22 +1,22 @@ -// import { ApiProperty, PartialType } from '@nestjs/swagger'; -// import { CreateEmployeeDto } from './create-employee.dto'; -// import { IsDateString, IsOptional, Max } from 'class-validator'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateEmployeeDto } from './create-employee.dto'; +import { IsDateString, IsOptional, Max } from 'class-validator'; -// export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { -// @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' }) -// @IsDateString() -// @IsOptional() -// first_work_day?: string; +export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { + @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' }) + @IsDateString() + @IsOptional() + first_work_day?: string; -// @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' }) -// @IsDateString() -// @IsOptional() -// last_work_day?: string; + @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' }) + @IsDateString() + @IsOptional() + last_work_day?: string; -// @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' }) -// @IsOptional() -// supervisor_id?: number; + @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' }) + @IsOptional() + supervisor_id?: number; -// @IsOptional() -// phone_number: string; -// } + @IsOptional() + phone_number: string; +} diff --git a/src/identity-and-account/employees/services/employees-archival.service.ts b/src/identity-and-account/employees/services/employees-archival.service.ts index 6046e94..2aa184a 100644 --- a/src/identity-and-account/employees/services/employees-archival.service.ts +++ b/src/identity-and-account/employees/services/employees-archival.service.ts @@ -1,173 +1,173 @@ -// import { Injectable } from "@nestjs/common"; -// import { Employees, EmployeesArchive, Users } from "@prisma/client"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { UpdateEmployeeDto } from "../dtos/update-employee.dto"; -// import { toDateOrUndefined, toDateOrNull } from "../utils/employee.utils"; +import { Injectable } from "@nestjs/common"; +import { Employees, Users } from "@prisma/client"; +import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto"; +import { toDateOrUndefined, toDateOrNull } from "src/identity-and-account/employees/utils/employee.utils"; +import { PrismaService } from "src/prisma/prisma.service"; -// @Injectable() -// export class EmployeesArchivalService { -// constructor(private readonly prisma: PrismaService) { } +@Injectable() +export class EmployeesArchivalService { + constructor(private readonly prisma: PrismaService) { } -// async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise { -// // 1) Tenter sur employés actifs -// const active = await this.prisma.employees.findFirst({ -// where: { user: { email } }, -// include: { user: true }, -// }); + async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise { + // 1) Tenter sur employés actifs + const active = await this.prisma.employees.findFirst({ + where: { user: { email } }, + include: { user: true }, + }); -// if (active) { -// // Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé -// if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) { -// return this.archiveOnTermination(active, dto); -// } + if (active) { + // Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé + // if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) { + // return this.archiveOnTermination(active, dto); + // } -// // Sinon, update standard (split Users/Employees) -// const { -// first_name, -// last_name, -// email: new_email, -// phone_number, -// residence, -// external_payroll_id, -// company_code, -// job_title, -// first_work_day, -// last_work_day, -// supervisor_id, -// is_supervisor, -// } = dto as any; + // Sinon, update standard (split Users/Employees) + const { + first_name, + last_name, + email: new_email, + phone_number, + residence, + external_payroll_id, + company_code, + job_title, + first_work_day, + last_work_day, + supervisor_id, + is_supervisor, + } = dto as any; -// const first_work_d = toDateOrUndefined(first_work_day); -// const last_work_d = Object.prototype.hasOwnProperty('last_work_day') -// ? toDateOrNull(last_work_day ?? null) -// : undefined; + const first_work_d = toDateOrUndefined(first_work_day); + const last_work_d = Object.prototype.hasOwnProperty('last_work_day') + ? toDateOrNull(last_work_day ?? null) + : undefined; -// await this.prisma.$transaction(async (transaction) => { -// if ( -// first_name !== undefined || -// last_name !== undefined || -// new_email !== undefined || -// phone_number !== undefined || -// residence !== undefined -// ) { -// await transaction.users.update({ -// where: { id: active.user_id }, -// data: { -// ...(first_name !== undefined ? { first_name } : {}), -// ...(last_name !== undefined ? { last_name } : {}), -// ...(email !== undefined ? { email: new_email } : {}), -// ...(phone_number !== undefined ? { phone_number } : {}), -// ...(residence !== undefined ? { residence } : {}), -// }, -// }); + await this.prisma.$transaction(async (transaction) => { + if ( + first_name !== undefined || + last_name !== undefined || + new_email !== undefined || + phone_number !== undefined || + residence !== undefined + ) { + await transaction.users.update({ + where: { id: active.user_id }, + data: { + ...(first_name !== undefined ? { first_name } : {}), + ...(last_name !== undefined ? { last_name } : {}), + ...(email !== undefined ? { email: new_email } : {}), + ...(phone_number !== undefined ? { phone_number } : {}), + ...(residence !== undefined ? { residence } : {}), + }, + }); -// } + } -// const updated = await transaction.employees.update({ -// where: { id: active.id }, -// data: { -// ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), -// ...(company_code !== undefined ? { company_code } : {}), -// ...(job_title !== undefined ? { job_title } : {}), -// ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}), -// ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}), -// ...(is_supervisor !== undefined ? { is_supervisor } : {}), -// ...(supervisor_id !== undefined ? { supervisor_id } : {}), -// }, -// include: { user: true }, -// }); + const updated = await transaction.employees.update({ + where: { id: active.id }, + data: { + ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), + ...(company_code !== undefined ? { company_code } : {}), + ...(job_title !== undefined ? { job_title } : {}), + ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}), + ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}), + ...(is_supervisor !== undefined ? { is_supervisor } : {}), + ...(supervisor_id !== undefined ? { supervisor_id } : {}), + }, + include: { user: true }, + }); -// return updated; -// }); + return updated; + }); -// return this.prisma.employees.findFirst({ where: { user: { email } } }); -// } + return this.prisma.employees.findFirst({ where: { user: { email } } }); + } -// const user = await this.prisma.users.findUnique({ where: { email } }); -// if (!user) return null; -// // 2) Pas trouvé en actifs → regarder en archive (pour restauration) -// const archived = await this.prisma.employeesArchive.findFirst({ -// where: { user_id: user.id }, -// include: { user: true }, -// }); + const user = await this.prisma.users.findUnique({ where: { email } }); + if (!user) return null; + // 2) Pas trouvé en actifs → regarder en archive (pour restauration) + // const archived = await this.prisma.employeesArchive.findFirst({ + // where: { user_id: user.id }, + // include: { user: true }, + // }); -// if (archived) { -// // Condition de restauration : last_work_day === null ou first_work_day fourni -// const restore = dto.last_work_day === null || dto.first_work_day != null; -// if (restore) { -// return this.restoreEmployee(archived, dto); -// } -// } -// // 3) Ni actif, ni archivé → 404 dans le controller -// return null; -// } + // if (archived) { + // // Condition de restauration : last_work_day === null ou first_work_day fourni + // const restore = dto.last_work_day === null || dto.first_work_day != null; + // if (restore) { + // return this.restoreEmployee(archived, dto); + // } + // } + // 3) Ni actif, ni archivé → 404 dans le controller + return null; + } -// //transfers the employee to archive and then delete from employees table -// private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise { -// const last_work_d = toDateOrNull(dto.last_work_day!); -// if (!last_work_d) throw new Error('invalide last_work_day for archive'); -// return this.prisma.$transaction(async transaction => { -// //detach crew from supervisor if employee is a supervisor -// await transaction.employees.updateMany({ -// where: { supervisor_id: active.id }, -// data: { supervisor_id: null }, -// }) -// const archived = await transaction.employeesArchive.create({ -// data: { -// employee_id: active.id, -// user_id: active.user_id, -// first_name: active.user.first_name, -// last_name: active.user.last_name, -// company_code: active.company_code, -// job_title: active.job_title, -// first_work_day: active.first_work_day, -// last_work_day: last_work_d, -// supervisor_id: active.supervisor_id ?? null, -// is_supervisor: active.is_supervisor, -// external_payroll_id: active.external_payroll_id, -// }, -// include: { user: true } -// }); -// //delete from employees table -// await transaction.employees.delete({ where: { id: active.id } }); -// //return archived employee -// return archived -// }); -// } + //transfers the employee to archive and then delete from employees table + // private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise { + // const last_work_d = toDateOrNull(dto.last_work_day!); + // if (!last_work_d) throw new Error('invalide last_work_day for archive'); + // return this.prisma.$transaction(async transaction => { + // //detach crew from supervisor if employee is a supervisor + // await transaction.employees.updateMany({ + // where: { supervisor_id: active.id }, + // data: { supervisor_id: null }, + // }) + // const archived = await transaction.employeesArchive.create({ + // data: { + // employee_id: active.id, + // user_id: active.user_id, + // first_name: active.user.first_name, + // last_name: active.user.last_name, + // company_code: active.company_code, + // job_title: active.job_title, + // first_work_day: active.first_work_day, + // last_work_day: last_work_d, + // supervisor_id: active.supervisor_id ?? null, + // is_supervisor: active.is_supervisor, + // external_payroll_id: active.external_payroll_id, + // }, + // include: { user: true } + // }); + // //delete from employees table + // await transaction.employees.delete({ where: { id: active.id } }); + // //return archived employee + // return archived + // }); + // } -// //transfers the employee from archive to the employees table -// private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise { -// // const first_work_d = toDateOrUndefined(dto.first_work_day); -// return this.prisma.$transaction(async transaction => { -// //restores the archived employee into the employees table -// const restored = await transaction.employees.create({ -// data: { -// user_id: archived.user_id, -// company_code: archived.company_code, -// job_title: archived.job_title, -// first_work_day: archived.first_work_day, -// last_work_day: null, -// is_supervisor: archived.is_supervisor ?? false, -// external_payroll_id: archived.external_payroll_id, -// }, -// }); -// //deleting archived entry by id -// await transaction.employeesArchive.delete({ where: { id: archived.id } }); + // //transfers the employee from archive to the employees table + // private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise { + // // const first_work_d = toDateOrUndefined(dto.first_work_day); + // return this.prisma.$transaction(async transaction => { + // //restores the archived employee into the employees table + // const restored = await transaction.employees.create({ + // data: { + // user_id: archived.user_id, + // company_code: archived.company_code, + // job_title: archived.job_title, + // first_work_day: archived.first_work_day, + // last_work_day: null, + // is_supervisor: archived.is_supervisor ?? false, + // external_payroll_id: archived.external_payroll_id, + // }, + // }); + // //deleting archived entry by id + // await transaction.employeesArchive.delete({ where: { id: archived.id } }); -// //return restored employee -// return restored; -// }); -// } + // //return restored employee + // return restored; + // }); + // } -// //fetches all archived employees -// async findAllArchived(): Promise { -// return this.prisma.employeesArchive.findMany(); -// } + // //fetches all archived employees + // async findAllArchived(): Promise { + // return this.prisma.employeesArchive.findMany(); + // } -// //fetches an archived employee -// async findOneArchived(id: number): Promise { -// return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } }); -// } + // //fetches an archived employee + // async findOneArchived(id: number): Promise { + // return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } }); + // } -// } +} diff --git a/src/identity-and-account/employees/services/employees.service.ts b/src/identity-and-account/employees/services/employees.service.ts index b44bf7d..50873b6 100644 --- a/src/identity-and-account/employees/services/employees.service.ts +++ b/src/identity-and-account/employees/services/employees.service.ts @@ -1,230 +1,230 @@ -// import { Injectable, NotFoundException } from '@nestjs/common'; -// import { PrismaService } from 'src/prisma/prisma.service'; -// import { EmployeeListItemDto } from '../dtos/list-employee.dto'; -// import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto'; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto"; +import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto"; +import { PrismaService } from "src/prisma/prisma.service"; -// @Injectable() -// export class EmployeesService { -// constructor(private readonly prisma: PrismaService) { } +@Injectable() +export class EmployeesService { + constructor(private readonly prisma: PrismaService) { } -// findListEmployees(): Promise { -// return this.prisma.employees.findMany({ -// select: { -// user: { -// select: { -// first_name: true, -// last_name: true, -// email: true, -// }, -// }, -// supervisor: { -// select: { -// user: { -// select: { -// first_name: true, -// last_name: true, -// }, -// }, -// }, -// }, -// job_title: true, -// company_code: true, -// } -// }).then(rows => rows.map(r => ({ -// first_name: r.user.first_name, -// last_name: r.user.last_name, -// email: r.user.email, -// company_name: r.company_code, -// job_title: r.job_title, -// employee_full_name: `${r.user.first_name} ${r.user.last_name}`, -// supervisor_full_name: r.supervisor ? `${r.supervisor.user.first_name} ${r.supervisor.user.last_name}` : null, -// })), -// ); -// } + findListEmployees(): Promise { + return this.prisma.employees.findMany({ + select: { + user: { + select: { + first_name: true, + last_name: true, + email: true, + }, + }, + supervisor: { + select: { + user: { + select: { + first_name: true, + last_name: true, + }, + }, + }, + }, + job_title: true, + company_code: true, + } + }).then(rows => rows.map(r => ({ + first_name: r.user.first_name, + last_name: r.user.last_name, + email: r.user.email, + company_name: r.company_code, + job_title: r.job_title, + employee_full_name: `${r.user.first_name} ${r.user.last_name}`, + supervisor_full_name: r.supervisor ? `${r.supervisor.user.first_name} ${r.supervisor.user.last_name}` : null, + })), + ); + } -// async findOneProfile(email: string): Promise { -// const emp = await this.prisma.employees.findFirst({ -// where: { user: { email } }, -// select: { -// user: { -// select: { -// first_name: true, -// last_name: true, -// email: true, -// phone_number: true, -// residence: true, -// }, -// }, -// supervisor: { -// select: { -// user: { -// select: { -// first_name: true, -// last_name: true, -// }, -// }, -// }, -// }, -// job_title: true, -// company_code: true, -// first_work_day: true, -// last_work_day: true, -// } -// }); -// if (!emp) throw new NotFoundException(`Employee with email ${email} not found`); + async findOneProfile(email: string): Promise { + const emp = await this.prisma.employees.findFirst({ + where: { user: { email } }, + select: { + user: { + select: { + first_name: true, + last_name: true, + email: true, + phone_number: true, + residence: true, + }, + }, + supervisor: { + select: { + user: { + select: { + first_name: true, + last_name: true, + }, + }, + }, + }, + job_title: true, + company_code: true, + first_work_day: true, + last_work_day: true, + } + }); + if (!emp) throw new NotFoundException(`Employee with email ${email} not found`); -// return { -// first_name: emp.user.first_name, -// last_name: emp.user.last_name, -// email: emp.user.email, -// residence: emp.user.residence, -// phone_number: emp.user.phone_number, -// company_name: emp.company_code, -// job_title: emp.job_title, -// employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`, -// first_work_day: emp.first_work_day.toISOString().slice(0, 10), -// last_work_day: emp.last_work_day ? emp.last_work_day.toISOString().slice(0, 10) : null, -// supervisor_full_name: emp.supervisor ? `${emp.supervisor.user.first_name}, ${emp.supervisor.user.last_name}` : null, -// }; -// } + return { + first_name: emp.user.first_name, + last_name: emp.user.last_name, + email: emp.user.email, + residence: emp.user.residence, + phone_number: emp.user.phone_number, + company_name: emp.company_code, + job_title: emp.job_title, + employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`, + first_work_day: emp.first_work_day.toISOString().slice(0, 10), + last_work_day: emp.last_work_day ? emp.last_work_day.toISOString().slice(0, 10) : null, + supervisor_full_name: emp.supervisor ? `${emp.supervisor.user.first_name}, ${emp.supervisor.user.last_name}` : null, + }; + } -// //_____________________________________________________________________________________________ -// // Deprecated or unused methods -// //_____________________________________________________________________________________________ + //_____________________________________________________________________________________________ + // Deprecated or unused methods + //_____________________________________________________________________________________________ -// // async create(dto: CreateEmployeeDto): Promise { -// // const { -// // first_name, -// // last_name, -// // email, -// // phone_number, -// // residence, -// // external_payroll_id, -// // company_code, -// // job_title, -// // first_work_day, -// // last_work_day, -// // is_supervisor, -// // } = dto; + // async create(dto: CreateEmployeeDto): Promise { + // const { + // first_name, + // last_name, + // email, + // phone_number, + // residence, + // external_payroll_id, + // company_code, + // job_title, + // first_work_day, + // last_work_day, + // is_supervisor, + // } = dto; -// // return this.prisma.$transaction(async (transaction) => { -// // const user: Users = await transaction.users.create({ -// // data: { -// // first_name, -// // last_name, -// // email, -// // phone_number, -// // residence, -// // }, -// // }); -// // return transaction.employees.create({ -// // data: { -// // user_id: user.id, -// // external_payroll_id, -// // company_code, -// // job_title, -// // first_work_day, -// // last_work_day, -// // is_supervisor, -// // }, -// // }); -// // }); -// // } + // return this.prisma.$transaction(async (transaction) => { + // const user: Users = await transaction.users.create({ + // data: { + // first_name, + // last_name, + // email, + // phone_number, + // residence, + // }, + // }); + // return transaction.employees.create({ + // data: { + // user_id: user.id, + // external_payroll_id, + // company_code, + // job_title, + // first_work_day, + // last_work_day, + // is_supervisor, + // }, + // }); + // }); + // } -// // findAll(): Promise { -// // return this.prisma.employees.findMany({ -// // include: { user: true }, -// // }); -// // } + // findAll(): Promise { + // return this.prisma.employees.findMany({ + // include: { user: true }, + // }); + // } -// // async findOne(email: string): Promise { -// // const emp = await this.prisma.employees.findFirst({ -// // where: { user: { email } }, -// // include: { user: true }, -// // }); + // async findOne(email: string): Promise { + // const emp = await this.prisma.employees.findFirst({ + // where: { user: { email } }, + // include: { user: true }, + // }); -// // //add search for archived employees -// // if (!emp) { -// // throw new NotFoundException(`Employee with email: ${email} not found`); -// // } -// // return emp; -// // } + // //add search for archived employees + // if (!emp) { + // throw new NotFoundException(`Employee with email: ${email} not found`); + // } + // return emp; + // } -// // async update( -// // email: string, -// // dto: UpdateEmployeeDto, -// // ): Promise { -// // const emp = await this.findOne(email); + // async update( + // email: string, + // dto: UpdateEmployeeDto, + // ): Promise { + // const emp = await this.findOne(email); -// // const { -// // first_name, -// // last_name, -// // phone_number, -// // residence, -// // external_payroll_id, -// // company_code, -// // job_title, -// // first_work_day, -// // last_work_day, -// // is_supervisor, -// // email: new_email, -// // } = dto; + // const { + // first_name, + // last_name, + // phone_number, + // residence, + // external_payroll_id, + // company_code, + // job_title, + // first_work_day, + // last_work_day, + // is_supervisor, + // email: new_email, + // } = dto; -// // return this.prisma.$transaction(async (transaction) => { -// // if( -// // first_name !== undefined || -// // last_name !== undefined || -// // new_email !== undefined || -// // phone_number !== undefined || -// // residence !== undefined -// // ){ -// // await transaction.users.update({ -// // where: { id: emp.user_id }, -// // data: { -// // ...(first_name !== undefined && { first_name }), -// // ...(last_name !== undefined && { last_name }), -// // ...(email !== undefined && { email }), -// // ...(phone_number !== undefined && { phone_number }), -// // ...(residence !== undefined && { residence }), -// // }, -// // }); -// // } + // return this.prisma.$transaction(async (transaction) => { + // if( + // first_name !== undefined || + // last_name !== undefined || + // new_email !== undefined || + // phone_number !== undefined || + // residence !== undefined + // ){ + // await transaction.users.update({ + // where: { id: emp.user_id }, + // data: { + // ...(first_name !== undefined && { first_name }), + // ...(last_name !== undefined && { last_name }), + // ...(email !== undefined && { email }), + // ...(phone_number !== undefined && { phone_number }), + // ...(residence !== undefined && { residence }), + // }, + // }); + // } -// // const updated = await transaction.employees.update({ -// // where: { id: emp.id }, -// // data: { -// // ...(external_payroll_id !== undefined && { external_payroll_id }), -// // ...(company_code !== undefined && { company_code }), -// // ...(first_work_day !== undefined && { first_work_day }), -// // ...(last_work_day !== undefined && { last_work_day }), -// // ...(job_title !== undefined && { job_title }), -// // ...(is_supervisor !== undefined && { is_supervisor }), -// // }, -// // }); -// // return updated; -// // }); -// // } + // const updated = await transaction.employees.update({ + // where: { id: emp.id }, + // data: { + // ...(external_payroll_id !== undefined && { external_payroll_id }), + // ...(company_code !== undefined && { company_code }), + // ...(first_work_day !== undefined && { first_work_day }), + // ...(last_work_day !== undefined && { last_work_day }), + // ...(job_title !== undefined && { job_title }), + // ...(is_supervisor !== undefined && { is_supervisor }), + // }, + // }); + // return updated; + // }); + // } -// // async remove(email: string): Promise { + // async remove(email: string): Promise { -// // const emp = await this.findOne(email); + // const emp = await this.findOne(email); -// // return this.prisma.$transaction(async (transaction) => { -// // await transaction.employees.updateMany({ -// // where: { supervisor_id: emp.id }, -// // data: { supervisor_id: null }, -// // }); -// // const deleted_employee = await transaction.employees.delete({ -// // where: {id: emp.id }, -// // }); -// // await transaction.users.delete({ -// // where: { id: emp.user_id }, -// // }); -// // return deleted_employee; -// // }); -// // } + // return this.prisma.$transaction(async (transaction) => { + // await transaction.employees.updateMany({ + // where: { supervisor_id: emp.id }, + // data: { supervisor_id: null }, + // }); + // const deleted_employee = await transaction.employees.delete({ + // where: {id: emp.id }, + // }); + // await transaction.users.delete({ + // where: { id: emp.user_id }, + // }); + // return deleted_employee; + // }); + // } -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/identity-and-account/employees/utils/employee.utils.ts b/src/identity-and-account/employees/utils/employee.utils.ts index 04f2540..3534f3d 100644 --- a/src/identity-and-account/employees/utils/employee.utils.ts +++ b/src/identity-and-account/employees/utils/employee.utils.ts @@ -1,9 +1,9 @@ -// export function toDateOrNull(v?: string | null): Date | null { -// if (!v) return null; -// const day = new Date(v); -// return isNaN(day.getTime()) ? null : day; -// } -// export function toDateOrUndefined(v?: string | null): Date | undefined { -// const day = toDateOrNull(v ?? undefined); -// return day === null ? undefined : day; -// } \ No newline at end of file +export function toDateOrNull(v?: string | null): Date | null { + if (!v) return null; + const day = new Date(v); + return isNaN(day.getTime()) ? null : day; +} +export function toDateOrUndefined(v?: string | null): Date | undefined { + const day = toDateOrNull(v ?? undefined); + return day === null ? undefined : day; +} \ No newline at end of file From 1a0532846f9d9ad50a90747a711b284b171d815d Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 5 Nov 2025 14:27:54 -0500 Subject: [PATCH 2/2] feat(role-guards): added role-guards group and added role check to controllers --- src/common/guards/ownership.guard.ts | 10 +- src/common/guards/roles.guard.ts | 6 +- src/common/shared/role-groupes.ts | 15 +++ .../controllers/employees.controller.ts | 24 ++--- .../services/abstract-user.service.ts | 58 +++++----- .../controller/schedule-presets.controller.ts | 29 ++--- .../shifts/controllers/shift.controller.ts | 11 +- .../shifts/services/shifts-upsert.service.ts | 100 +++++++++--------- .../controllers/timesheet.controller.ts | 25 +++-- 9 files changed, 146 insertions(+), 132 deletions(-) create mode 100644 src/common/shared/role-groupes.ts diff --git a/src/common/guards/ownership.guard.ts b/src/common/guards/ownership.guard.ts index 9fcba18..27928bc 100644 --- a/src/common/guards/ownership.guard.ts +++ b/src/common/guards/ownership.guard.ts @@ -1,4 +1,4 @@ -import { +import { CanActivate, Injectable, ExecutionContext, @@ -17,15 +17,15 @@ export class OwnershipGuard implements CanActivate { constructor( private reflector: Reflector, private moduleRef: ModuleRef, - ) {} + ) { } - async canActivate(context: ExecutionContext): Promise{ + async canActivate(context: ExecutionContext): Promise { const meta = this.reflector.get( OWNER_KEY, context.getHandler(), ); - if (!meta) + if (!meta) return true; - + const request = context.switchToHttp().getRequest(); const user = request.user; const resourceId = request.params[meta.idParam || 'id']; diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index 61889f7..9a9244d 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -17,7 +17,7 @@ interface RequestWithUser extends Request { @Injectable() export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor(private reflector: Reflector) { } /** * @swagger @@ -37,9 +37,9 @@ export class RolesGuard implements CanActivate { * or returns `false` if the user is not authenticated. */ canActivate(ctx: ExecutionContext): boolean { - const requiredRoles = this.reflector.get( + const requiredRoles = this.reflector.getAllAndOverride( ROLES_KEY, - ctx.getHandler(), + [ctx.getHandler(), ctx.getClass()], ); //for "deny-by-default" when role is wrong or unavailable if (!requiredRoles || requiredRoles.length === 0) { diff --git a/src/common/shared/role-groupes.ts b/src/common/shared/role-groupes.ts new file mode 100644 index 0000000..b67c432 --- /dev/null +++ b/src/common/shared/role-groupes.ts @@ -0,0 +1,15 @@ +import { Roles as RoleEnum } from ".prisma/client"; + +export const GLOBAL_CONTROLLER_ROLES: readonly RoleEnum[] = [ + RoleEnum.EMPLOYEE, + RoleEnum.ACCOUNTING, + RoleEnum.HR, + RoleEnum.SUPERVISOR, + RoleEnum.ADMIN, +]; + +export const MANAGER_ROLES: readonly RoleEnum[] = [ + RoleEnum.HR, + RoleEnum.SUPERVISOR, + RoleEnum.ADMIN, +] \ No newline at end of file diff --git a/src/identity-and-account/employees/controllers/employees.controller.ts b/src/identity-and-account/employees/controllers/employees.controller.ts index dabca96..fa45089 100644 --- a/src/identity-and-account/employees/controllers/employees.controller.ts +++ b/src/identity-and-account/employees/controllers/employees.controller.ts @@ -1,37 +1,35 @@ import { Controller, Get, Patch, Param, Body, NotFoundException } from "@nestjs/common"; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto"; import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto"; import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto"; import { EmployeesArchivalService } from "src/identity-and-account/employees/services/employees-archival.service"; import { EmployeesService } from "src/identity-and-account/employees/services/employees.service"; -@ApiBearerAuth('access-token') -// @UseGuards() +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) @Controller('employees') export class EmployeesController { constructor( private readonly employeesService: EmployeesService, - private readonly archiveService: EmployeesArchivalService, - ) {} + private readonly archiveService: EmployeesArchivalService, + ) { } @Get('employee-list') - //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) - + @RolesAllowed(...MANAGER_ROLES) findListEmployees(): Promise { return this.employeesService.findListEmployees(); } @Patch(':email') - //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiBearerAuth('access-token') + @RolesAllowed(...MANAGER_ROLES) async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) { // if last_work_day is set => archive the employee // else if employee is archived and first_work_day or last_work_day = null => restore //otherwise => standard update const result = await this.archiveService.patchEmployee(email, dto); - if(!result) { - throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`) + if (!result) { + throw new NotFoundException(`Employee with email: ${email} is not found in active or archive.`) } return result; } @@ -68,10 +66,6 @@ export class EmployeesController { // } @Get('profile/:email') - @ApiOperation({summary: 'Find employee profile' }) - @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) - @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) - @ApiResponse({ status: 400, description: 'Employee profile not found' }) findOneProfile(@Param('email') email: string): Promise { return this.employeesService.findOneProfile(email); } diff --git a/src/identity-and-account/users-management/services/abstract-user.service.ts b/src/identity-and-account/users-management/services/abstract-user.service.ts index 4e0da2e..9a2ffd1 100644 --- a/src/identity-and-account/users-management/services/abstract-user.service.ts +++ b/src/identity-and-account/users-management/services/abstract-user.service.ts @@ -4,38 +4,38 @@ import { PrismaService } from 'src/prisma/prisma.service'; @Injectable() export abstract class AbstractUserService { - constructor(protected readonly prisma: PrismaService) {} + constructor(protected readonly prisma: PrismaService) { } - findAll(): Promise { - return this.prisma.users.findMany(); - } + findAll(): Promise { + return this.prisma.users.findMany(); + } - async findOne( id: string ): Promise { - const user = await this.prisma.users.findUnique({ where: { id } }); - if (!user) { - throw new NotFoundException(`User #${id} not found`); - } - return user; - } + async findOne(id: string): Promise { + const user = await this.prisma.users.findUnique({ where: { id } }); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } - async findOneByEmail( email: string ): Promise> { - const user = await this.prisma.users.findUnique({ where: { email } }); - if (!user) { - throw new NotFoundException(`No user with email #${email} exists`); - } + async findOneByEmail(email: string): Promise> { + const user = await this.prisma.users.findUnique({ where: { email } }); + if (!user) { + throw new NotFoundException(`No user with email #${email} exists`); + } - const clean_user = { - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - role: user.role, - } - - return clean_user; - } + const clean_user = { + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + role: user.role, + } - async remove(id: string): Promise { - await this.findOne(id); - return this.prisma.users.delete({ where: { id } }); - } + return clean_user; + } + + async remove(id: string): Promise { + await this.findOne(id); + return this.prisma.users.delete({ where: { id } }); + } } diff --git a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts index 042b8df..d96cf17 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts @@ -6,53 +6,54 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracke import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; import { Roles as RoleEnum } from '.prisma/client'; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; @Controller('schedule-presets') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class SchedulePresetsController { constructor( - private readonly upsertService: SchedulePresetsUpsertService, - private readonly getService: SchedulePresetsGetService, + private readonly upsertService: SchedulePresetsUpsertService, + private readonly getService: SchedulePresetsGetService, private readonly applyPresetsService: SchedulePresetsApplyService, - ){} + ) { } //used to create a schedule preset @Post('create') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) - async createPreset( @Req() req, @Body() dto: SchedulePresetsDto ) { + @RolesAllowed(...MANAGER_ROLES) + async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { const email = req.user?.email; return await this.upsertService.createPreset(email, dto); } //used to update an already existing schedule preset @Patch('update/:preset_id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) - async updatePreset( @Param('preset_id', ParseIntPipe) preset_id: number,@Body() dto: SchedulePresetsUpdateDto ) { + @RolesAllowed(...MANAGER_ROLES) + async updatePreset(@Param('preset_id', ParseIntPipe) preset_id: number, @Body() dto: SchedulePresetsUpdateDto) { return await this.upsertService.updatePreset(preset_id, dto); } //used to delete a schedule preset @Delete('delete/:preset_id') @RolesAllowed(RoleEnum.ADMIN) - async deletePreset( @Param('preset_id') preset_id: number ) { + async deletePreset(@Param('preset_id') preset_id: number) { return await this.upsertService.deletePreset(preset_id); } //used to show the list of available schedule presets @Get('find-list') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) - async findListById( @Req() req) { + @RolesAllowed(...MANAGER_ROLES) + async findListById(@Req() req) { const email = req.user?.email; return this.getService.getSchedulePresets(email); } //used to apply a preset to a timesheet @Post('apply-presets') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) - async applyPresets( @Req() req, @Query('preset') preset_name: string, @Query('start') start_date: string ) { + async applyPresets(@Req() req, @Query('preset') preset_name: string, @Query('start') start_date: string) { const email = req.user?.email; - if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); - if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); + if (!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); + if (!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); return this.applyPresetsService.applyToTimesheet(email, preset_name, start_date); } } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index b9ce63d..886536b 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -3,17 +3,18 @@ import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; -import { Roles as RoleEnum } from '.prisma/client'; import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; + @Controller('shift') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class ShiftController { constructor( private readonly upsert_service: ShiftsUpsertService, ){} @Post('create') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise { const email = req.user?.email; const list = Array.isArray(dtos) ? dtos : []; @@ -21,10 +22,7 @@ export class ShiftController { return this.upsert_service.createShifts(email, dtos) } - - //change Body to receive dtos @Patch('update') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) updateBatch( @Body() dtos: UpdateShiftDto[]): Promise{ const list = Array.isArray(dtos) ? dtos: []; if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); @@ -32,9 +30,8 @@ export class ShiftController { } @Delete(':shift_id') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) remove(@Param('shift_id') shift_id: number ) { return this.upsert_service.deleteShift(shift_id); } - + } diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts index e1d36f4..0a2e96c 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts @@ -41,16 +41,16 @@ export class ShiftsUpsertService { if (normed.end_time <= normed.start_time) { const error = { error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), date: toStringFromDate(normed.date), }, }; return { index, error }; } - if(!normed.end_time) throw new BadRequestException('A shift needs an end_time'); - if(!normed.start_time) throw new BadRequestException('A shift needs a start_time'); + if (!normed.end_time) throw new BadRequestException('A shift needs an end_time'); + if (!normed.start_time) throw new BadRequestException('A shift needs a start_time'); const timesheet = await this.prisma.timesheets.findUnique({ where: { id: dto.timesheet_id, employee_id }, @@ -59,9 +59,9 @@ export class ShiftsUpsertService { if (!timesheet) { const error = { error_code: 'INVALID_TIMESHEET', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), date: toStringFromDate(normed.date), }, }; @@ -116,14 +116,14 @@ export class ShiftsUpsertService { if ( overlaps( { start: ordered[j - 1].start, end: ordered[j - 1].end, date: ordered[j - 1].date }, - { start: ordered[j].start, end: ordered[j].end, date: ordered[j].date }, + { start: ordered[j].start, end: ordered[j].end, date: ordered[j].date }, ) ) { const error = new ConflictException({ error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(ordered[j].start), - end_time: toStringFromHHmm(ordered[j].end), + conflicts: { + start_time: toStringFromHHmm(ordered[j].start), + end_time: toStringFromHHmm(ordered[j].end), date: toStringFromDate(ordered[j].date), }, }); @@ -148,7 +148,7 @@ export class ShiftsUpsertService { where: { timesheet_id, date: day_date }, select: { start_time: true, end_time: true, id: true, date: true }, }); - existing_map.set( key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date }))); + existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date }))); } normed_shifts.forEach((x, i) => { @@ -164,8 +164,8 @@ export class ShiftsUpsertService { existing = []; existing_map.set(map_key, existing); } - const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.date }, - { start: normed.start_time, end: normed.end_time, date:normed.date})); + const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.date }, + { start: normed.start_time, end: normed.end_time, date: normed.date })); if (hit) { results[index] = { ok: false, @@ -201,7 +201,7 @@ export class ShiftsUpsertService { }; existing.push(normalized_row); existing_map.set(map_key, existing); - + const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); @@ -236,11 +236,11 @@ export class ShiftsUpsertService { // recalculate overtime after update // return an updated version to display async updateShifts(dtos: UpdateShiftDto[]): Promise { - if (!Array.isArray(dtos) || dtos.length === 0) return []; + if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' }); const updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => { const { id, ...rest } = item; - if (!Number.isInteger(id)) throw new ConflictException({ error_code: 'INVALID_SHIFT'}); + if (!id) throw new BadRequestException({ error_code: 'SHIFT_INVALID' }); const changes: UpdateShiftChanges = {}; if (rest.date !== undefined) changes.date = rest.date; @@ -265,13 +265,15 @@ export class ShiftsUpsertService { const existing = regroup_id.get(update.id); if (!existing) { return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new NotFoundException(`Shift with id: ${update.id} not found`) } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to missing shift') })); + ? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + ); } if (existing.is_approved) { return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new BadRequestException('Approved shift cannot be updated') } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to approved shift in update set') })); + ? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + ); } } @@ -307,12 +309,14 @@ export class ShiftsUpsertService { where: { timesheet_id: group.timesheet_id, date: day_date }, select: { id: true, start_time: true, end_time: true, date: true }, }); - groups.set(key(group.timesheet_id, day_date), { existing: existing.map(row => ({ - id: row.id, - start: row.start_time, - end: row.end_time, - date: row.date, - })), incoming: planned_updates }); + groups.set(key(group.timesheet_id, day_date), { + existing: existing.map(row => ({ + id: row.id, + start: row.start_time, + end: row.end_time, + date: row.date, + })), incoming: planned_updates + }); } for (const planned of planned_updates) { @@ -320,23 +324,23 @@ export class ShiftsUpsertService { const group = groups.get(keys)!; const conflict = group.existing.find(row => - row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end, date: row.date }, + row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end, date: row.date }, { start: planned.normed.start_time, end: planned.normed.end_time, date: planned.normed.date }) ); if (conflict) { return updates.map(exist => exist.id === planned.exist_shift.id ? ({ - ok: false, id: exist.id, error:{ + ok: false, id: exist.id, error: { error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(conflict.start), - end_time: toStringFromHHmm(conflict.end), + conflicts: { + start_time: toStringFromHHmm(conflict.start), + end_time: toStringFromHHmm(conflict.end), date: toStringFromDate(conflict.date), }, } } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') }) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_OVERLAP' }) }) ); } } @@ -345,29 +349,29 @@ export class ShiftsUpsertService { for (const planned of planned_updates) { const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []); - regoup_by_day.get(keys)!.push({ - id: planned.exist_shift.id, - start: planned.normed.start_time, - end: planned.normed.end_time, - date: planned.normed.date + regoup_by_day.get(keys)!.push({ + id: planned.exist_shift.id, + start: planned.normed.start_time, + end: planned.normed.end_time, + date: planned.normed.date }); } - + for (const arr of regoup_by_day.values()) { arr.sort((a, b) => a.start.getTime() - b.start.getTime()); for (let i = 1; i < arr.length; i++) { if (overlaps( - { start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].date }, - { start: arr[i].start, end: arr[i].end, date: arr[i].date }) + { start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].date }, + { start: arr[i].start, end: arr[i].end, date: arr[i].date }) ) { - const error = { + const error = { error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(arr[i].start), - end_time: toStringFromHHmm(arr[i].end), + conflicts: { + start_time: toStringFromHHmm(arr[i].start), + end_time: toStringFromHHmm(arr[i].end), date: toStringFromDate(arr[i].date), }, - + }; return updates.map(exist => ({ ok: false, id: exist.id, error: error })); } @@ -426,7 +430,7 @@ export class ShiftsUpsertService { where: { id: shift_id }, select: { id: true, date: true, timesheet_id: true }, }); - if (!shift) throw new ConflictException({ error_code: 'INVALID_SHIFT'}); + if (!shift) throw new ConflictException({ error_code: 'SHIFT_INVALID' }); await tx.shifts.delete({ where: { id: shift_id } }); diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts index 849a121..96bb419 100644 --- a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts +++ b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts @@ -1,31 +1,34 @@ -import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException} from "@nestjs/common"; +import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; -import { Roles as RoleEnum } from '.prisma/client'; import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; @Controller('timesheets') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class TimesheetController { - constructor( + constructor( private readonly timesheetOverview: GetTimesheetsOverviewService, private readonly approvalService: TimesheetApprovalService, - ){} + ) { } @Get() - @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) - async getTimesheetByIds( - @Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) { + getTimesheetByPayPeriod( + @Req() req, + @Query('year', ParseIntPipe) year: number, + @Query('period_number', ParseIntPipe) period_number: number + ) { const email = req.user?.email; - if(!email) throw new UnauthorizedException('Unauthorized User'); + if (!email) throw new UnauthorizedException('Unauthorized User'); return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number); } @Patch('timesheet-approval') - @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) - async approveTimesheet( + @RolesAllowed(...MANAGER_ROLES) + approveTimesheet( @Body('timesheet_id', ParseIntPipe) timesheet_id: number, - @Body('is_approved' , ParseBoolPipe) is_approved: boolean, + @Body('is_approved', ParseBoolPipe) is_approved: boolean, ) { return this.approvalService.approveTimesheetById(timesheet_id, is_approved); }