Merge branch 'main' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Nicolas Drolet 2025-11-05 14:34:30 -05:00
commit 032e1de631
16 changed files with 723 additions and 720 deletions

View File

@ -1,4 +1,4 @@
import { import {
CanActivate, CanActivate,
Injectable, Injectable,
ExecutionContext, ExecutionContext,
@ -17,15 +17,15 @@ export class OwnershipGuard implements CanActivate {
constructor( constructor(
private reflector: Reflector, private reflector: Reflector,
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
) {} ) { }
async canActivate(context: ExecutionContext): Promise<boolean>{ async canActivate(context: ExecutionContext): Promise<boolean> {
const meta = this.reflector.get<OwnershipMeta>( const meta = this.reflector.get<OwnershipMeta>(
OWNER_KEY, context.getHandler(), OWNER_KEY, context.getHandler(),
); );
if (!meta) if (!meta)
return true; return true;
const request = context.switchToHttp().getRequest<RequestWithUser>(); const request = context.switchToHttp().getRequest<RequestWithUser>();
const user = request.user; const user = request.user;
const resourceId = request.params[meta.idParam || 'id']; const resourceId = request.params[meta.idParam || 'id'];

View File

@ -17,7 +17,7 @@ interface RequestWithUser extends Request {
@Injectable() @Injectable()
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} constructor(private reflector: Reflector) { }
/** /**
* @swagger * @swagger
@ -37,9 +37,9 @@ export class RolesGuard implements CanActivate {
* or returns `false` if the user is not authenticated. * or returns `false` if the user is not authenticated.
*/ */
canActivate(ctx: ExecutionContext): boolean { canActivate(ctx: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<Roles[]>( const requiredRoles = this.reflector.getAllAndOverride<Roles[]>(
ROLES_KEY, ROLES_KEY,
ctx.getHandler(), [ctx.getHandler(), ctx.getClass()],
); );
//for "deny-by-default" when role is wrong or unavailable //for "deny-by-default" when role is wrong or unavailable
if (!requiredRoles || requiredRoles.length === 0) { if (!requiredRoles || requiredRoles.length === 0) {

View File

@ -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,
]

View File

@ -1,99 +1,83 @@
// import { Body,Controller,Get,NotFoundException,Param,Patch } from '@nestjs/common'; import { Controller, Get, Patch, Param, Body, NotFoundException } from "@nestjs/common";
// import { EmployeesService } from '../services/employees.service'; import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { CreateEmployeeDto } from '../dtos/create-employee.dto'; import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
// import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto";
// import { RolesAllowed } from '../../../common/decorators/roles.decorators'; import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto";
// import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto";
// import { EmployeeListItemDto } from '../dtos/list-employee.dto'; import { EmployeesArchivalService } from "src/identity-and-account/employees/services/employees-archival.service";
// import { EmployeesArchivalService } from '../services/employees-archival.service'; import { EmployeesService } from "src/identity-and-account/employees/services/employees.service";
// import { EmployeeProfileItemDto } from 'src/modules/employees/dtos/profil-employee.dto';
// @ApiTags('Employees') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
// @ApiBearerAuth('access-token') @Controller('employees')
// // @UseGuards() export class EmployeesController {
// @Controller('employees') constructor(
// export class EmployeesController { private readonly employeesService: EmployeesService,
// constructor( private readonly archiveService: EmployeesArchivalService,
// private readonly employeesService: EmployeesService, ) { }
// private readonly archiveService: EmployeesArchivalService,
// ) {}
// @Get('employee-list') @Get('employee-list')
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) @RolesAllowed(...MANAGER_ROLES)
// @ApiOperation({summary: 'Find all employees with scoped info' }) findListEmployees(): Promise<EmployeeListItemDto[]> {
// @ApiResponse({ status: 200, description: 'List of employees with scoped info found', type: EmployeeListItemDto, isArray: true }) return this.employeesService.findListEmployees();
// @ApiResponse({ status: 400, description: 'List of employees with scoped info not found' }) }
// findListEmployees(): Promise<EmployeeListItemDto[]> {
// return this.employeesService.findListEmployees();
// }
// @Patch(':email') @Patch(':email')
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(...MANAGER_ROLES)
// @ApiBearerAuth('access-token') async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) {
// @ApiOperation({ summary: 'Update, archive or restore an employee' }) // if last_work_day is set => archive the employee
// @ApiParam({ name: 'email', type: Number, description: 'Email of the employee' }) // else if employee is archived and first_work_day or last_work_day = null => restore
// @ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto }) //otherwise => standard update
// @ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto }) const result = await this.archiveService.patchEmployee(email, dto);
// @ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) if (!result) {
// async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) { throw new NotFoundException(`Employee with email: ${email} is not found in active or archive.`)
// // if last_work_day is set => archive the employee }
// // else if employee is archived and first_work_day or last_work_day = null => restore return result;
// //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;
// }
// //_____________________________________________________________________________________________ //_____________________________________________________________________________________________
// // Deprecated or unused methods // Deprecated or unused methods
// //_____________________________________________________________________________________________ //_____________________________________________________________________________________________
// // @Post() // @Post()
// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// // @ApiOperation({summary: 'Create employee' }) // @ApiOperation({summary: 'Create employee' })
// // @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto }) // @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto })
// // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
// // create(@Body() dto: CreateEmployeeDto): Promise<Employees> { // create(@Body() dto: CreateEmployeeDto): Promise<Employees> {
// // return this.employeesService.create(dto); // return this.employeesService.create(dto);
// // } // }
// // @Get() // @Get()
// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
// // @ApiOperation({summary: 'Find all employees' }) // @ApiOperation({summary: 'Find all employees' })
// // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true }) // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true })
// // @ApiResponse({ status: 400, description: 'List of employees not found' }) // @ApiResponse({ status: 400, description: 'List of employees not found' })
// // findAll(): Promise<Employees[]> { // findAll(): Promise<Employees[]> {
// // return this.employeesService.findAll(); // return this.employeesService.findAll();
// // } // }
// // @Get(':email') // @Get(':email')
// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
// // @ApiOperation({summary: 'Find employee' }) // @ApiOperation({summary: 'Find employee' })
// // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
// // @ApiResponse({ status: 400, description: 'Employee not found' }) // @ApiResponse({ status: 400, description: 'Employee not found' })
// // findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> { // findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
// // return this.employeesService.findOne(email); // return this.employeesService.findOne(email);
// // } // }
// @Get('profile/:email') @Get('profile/:email')
// @ApiOperation({summary: 'Find employee profile' }) findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
// @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) return this.employeesService.findOneProfile(email);
// @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) }
// @ApiResponse({ status: 400, description: 'Employee profile not found' })
// findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
// return this.employeesService.findOneProfile(email);
// }
// // @Delete(':email') // @Delete(':email')
// // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
// // @ApiOperation({summary: 'Delete employee' }) // @ApiOperation({summary: 'Delete employee' })
// // @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' }) // @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' })
// // @ApiResponse({ status: 204, description: 'Employee deleted' }) // @ApiResponse({ status: 204, description: 'Employee deleted' })
// // @ApiResponse({ status: 404, description: 'Employee not found' }) // @ApiResponse({ status: 404, description: 'Employee not found' })
// // remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> { // remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
// // return this.employeesService.remove(email); // return this.employeesService.remove(email);
// // } // }
// } }

View File

@ -1,118 +1,118 @@
// import { import {
// Allow, Allow,
// IsBoolean, IsBoolean,
// IsDateString, IsDateString,
// IsEmail, IsEmail,
// IsInt, IsInt,
// IsNotEmpty, IsNotEmpty,
// IsOptional, IsOptional,
// IsPositive, IsPositive,
// IsString, IsString,
// IsUUID, IsUUID,
// } from 'class-validator'; } from 'class-validator';
// import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
// import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
// export class CreateEmployeeDto { export class CreateEmployeeDto {
// @ApiProperty({ @ApiProperty({
// example: 1, example: 1,
// description: 'Unique ID of an employee(primary-key, auto-incremented)', description: 'Unique ID of an employee(primary-key, auto-incremented)',
// }) })
// @Allow() @Allow()
// id: number; id: number;
// @ApiProperty({ @ApiProperty({
// example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d',
// description: 'UUID of the user linked to that employee', description: 'UUID of the user linked to that employee',
// }) })
// @IsUUID() @IsUUID()
// @IsOptional() @IsOptional()
// user_id?: string; user_id?: string;
// @ApiProperty({ @ApiProperty({
// example: 'Frodo', example: 'Frodo',
// description: 'Employee`s first name', description: 'Employee`s first name',
// }) })
// @IsString() @IsString()
// @IsNotEmpty() @IsNotEmpty()
// first_name: string; first_name: string;
// @ApiProperty({ @ApiProperty({
// example: 'Baggins', example: 'Baggins',
// description: 'Employee`s last name', description: 'Employee`s last name',
// }) })
// @IsString() @IsString()
// @IsNotEmpty() @IsNotEmpty()
// last_name: string; last_name: string;
// @ApiProperty({ @ApiProperty({
// example: 'i_cant_do_this_sam@targointernet.com', example: 'i_cant_do_this_sam@targointernet.com',
// description: 'Employee`s email', description: 'Employee`s email',
// }) })
// @IsEmail() @IsEmail()
// @IsOptional() @IsOptional()
// email: string; email: string;
// @IsOptional() @IsOptional()
// @IsBoolean() @IsBoolean()
// is_supervisor: boolean; is_supervisor: boolean;
// @ApiProperty({ @ApiProperty({
// example: '82538437464', example: '82538437464',
// description: 'Employee`s phone number', description: 'Employee`s phone number',
// }) })
// @IsString() @IsString()
// phone_number: string; phone_number: string;
// @ApiProperty({ @ApiProperty({
// example: '1 Bagshot Row, Hobbiton, The Shire, Middle-earth', example: '1 Bagshot Row, Hobbiton, The Shire, Middle-earth',
// description: 'Employee`s residence', description: 'Employee`s residence',
// required: false, required: false,
// }) })
// @IsString() @IsString()
// @IsOptional() @IsOptional()
// residence?: string; residence?: string;
// @ApiProperty({ @ApiProperty({
// example: 7464, example: 7464,
// description: 'external ID for the pay system', description: 'external ID for the pay system',
// }) })
// @IsInt() @IsInt()
// @IsPositive() @IsPositive()
// @Type(() => Number) @Type(() => Number)
// external_payroll_id: number; external_payroll_id: number;
// @ApiProperty({ @ApiProperty({
// example: 335567447, example: 335567447,
// description: 'Employee`s company code', description: 'Employee`s company code',
// }) })
// @IsInt() @IsInt()
// @IsPositive() @IsPositive()
// @Type(() => Number) @Type(() => Number)
// company_code: number; company_code: number;
// @ApiProperty({ @ApiProperty({
// example:'technicient', example:'technicient',
// description: 'employee`s job title', description: 'employee`s job title',
// }) })
// @IsString() @IsString()
// @IsOptional() @IsOptional()
// job_title: string; job_title: string;
// @ApiProperty({ @ApiProperty({
// example: '23/09/3018', example: '23/09/3018',
// description: 'Employee`s first working day', description: 'Employee`s first working day',
// }) })
// @IsDateString() @IsDateString()
// first_work_day: string; first_work_day: string;
// @ApiProperty({ @ApiProperty({
// example: '25/03/3019', example: '25/03/3019',
// description: 'Employee`s last working day', description: 'Employee`s last working day',
// required: false, required: false,
// }) })
// @IsDateString() @IsDateString()
// @IsOptional() @IsOptional()
// last_work_day?: string; last_work_day?: string;
// } }

View File

@ -1,8 +1,8 @@
// export class EmployeeListItemDto { export class EmployeeListItemDto {
// first_name: string; first_name: string;
// last_name: string; last_name: string;
// email: string; email: string;
// supervisor_full_name: string | null; supervisor_full_name: string | null;
// company_name: number | null; company_name: number | null;
// job_title: string | null; job_title: string | null;
// } }

View File

@ -1,13 +1,13 @@
// export class EmployeeProfileItemDto { export class EmployeeProfileItemDto {
// first_name: string; first_name: string;
// last_name: string; last_name: string;
// employee_full_name: string; employee_full_name: string;
// supervisor_full_name: string | null; supervisor_full_name: string | null;
// company_name: number | null; company_name: number | null;
// job_title: string | null; job_title: string | null;
// email: string | null; email: string | null;
// phone_number: string; phone_number: string;
// first_work_day: string; first_work_day: string;
// last_work_day?: string | null; last_work_day?: string | null;
// residence: string | null; residence: string | null;
// } }

View File

@ -1,22 +1,22 @@
// import { ApiProperty, PartialType } from '@nestjs/swagger'; import { ApiProperty, PartialType } from '@nestjs/swagger';
// import { CreateEmployeeDto } from './create-employee.dto'; import { CreateEmployeeDto } from './create-employee.dto';
// import { IsDateString, IsOptional, Max } from 'class-validator'; import { IsDateString, IsOptional, Max } from 'class-validator';
// export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {
// @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' }) @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' })
// @IsDateString() @IsDateString()
// @IsOptional() @IsOptional()
// first_work_day?: string; first_work_day?: string;
// @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' }) @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' })
// @IsDateString() @IsDateString()
// @IsOptional() @IsOptional()
// last_work_day?: string; last_work_day?: string;
// @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' }) @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' })
// @IsOptional() @IsOptional()
// supervisor_id?: number; supervisor_id?: number;
// @IsOptional() @IsOptional()
// phone_number: string; phone_number: string;
// } }

View File

@ -1,173 +1,173 @@
// import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
// import { Employees, EmployeesArchive, Users } from "@prisma/client"; import { Employees, Users } from "@prisma/client";
// import { PrismaService } from "src/prisma/prisma.service"; import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto";
// import { UpdateEmployeeDto } from "../dtos/update-employee.dto"; import { toDateOrUndefined, toDateOrNull } from "src/identity-and-account/employees/utils/employee.utils";
// import { toDateOrUndefined, toDateOrNull } from "../utils/employee.utils"; import { PrismaService } from "src/prisma/prisma.service";
// @Injectable() @Injectable()
// export class EmployeesArchivalService { export class EmployeesArchivalService {
// constructor(private readonly prisma: PrismaService) { } constructor(private readonly prisma: PrismaService) { }
// async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> { async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | null> {
// // 1) Tenter sur employés actifs // 1) Tenter sur employés actifs
// const active = await this.prisma.employees.findFirst({ const active = await this.prisma.employees.findFirst({
// where: { user: { email } }, where: { user: { email } },
// include: { user: true }, include: { user: true },
// }); });
// if (active) { if (active) {
// // Archivage : si on reçoit un last_work_day défini et que l'employé nest pas déjà terminé // Archivage : si on reçoit un last_work_day défini et que l'employé nest pas déjà terminé
// if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) { // if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) {
// return this.archiveOnTermination(active, dto); // return this.archiveOnTermination(active, dto);
// } // }
// // Sinon, update standard (split Users/Employees) // Sinon, update standard (split Users/Employees)
// const { const {
// first_name, first_name,
// last_name, last_name,
// email: new_email, email: new_email,
// phone_number, phone_number,
// residence, residence,
// external_payroll_id, external_payroll_id,
// company_code, company_code,
// job_title, job_title,
// first_work_day, first_work_day,
// last_work_day, last_work_day,
// supervisor_id, supervisor_id,
// is_supervisor, is_supervisor,
// } = dto as any; } = dto as any;
// const first_work_d = toDateOrUndefined(first_work_day); const first_work_d = toDateOrUndefined(first_work_day);
// const last_work_d = Object.prototype.hasOwnProperty('last_work_day') const last_work_d = Object.prototype.hasOwnProperty('last_work_day')
// ? toDateOrNull(last_work_day ?? null) ? toDateOrNull(last_work_day ?? null)
// : undefined; : undefined;
// await this.prisma.$transaction(async (transaction) => { await this.prisma.$transaction(async (transaction) => {
// if ( if (
// first_name !== undefined || first_name !== undefined ||
// last_name !== undefined || last_name !== undefined ||
// new_email !== undefined || new_email !== undefined ||
// phone_number !== undefined || phone_number !== undefined ||
// residence !== undefined residence !== undefined
// ) { ) {
// await transaction.users.update({ await transaction.users.update({
// where: { id: active.user_id }, where: { id: active.user_id },
// data: { data: {
// ...(first_name !== undefined ? { first_name } : {}), ...(first_name !== undefined ? { first_name } : {}),
// ...(last_name !== undefined ? { last_name } : {}), ...(last_name !== undefined ? { last_name } : {}),
// ...(email !== undefined ? { email: new_email } : {}), ...(email !== undefined ? { email: new_email } : {}),
// ...(phone_number !== undefined ? { phone_number } : {}), ...(phone_number !== undefined ? { phone_number } : {}),
// ...(residence !== undefined ? { residence } : {}), ...(residence !== undefined ? { residence } : {}),
// }, },
// }); });
// } }
// const updated = await transaction.employees.update({ const updated = await transaction.employees.update({
// where: { id: active.id }, where: { id: active.id },
// data: { data: {
// ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), ...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
// ...(company_code !== undefined ? { company_code } : {}), ...(company_code !== undefined ? { company_code } : {}),
// ...(job_title !== undefined ? { job_title } : {}), ...(job_title !== undefined ? { job_title } : {}),
// ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}), ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}),
// ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}), ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}),
// ...(is_supervisor !== undefined ? { is_supervisor } : {}), ...(is_supervisor !== undefined ? { is_supervisor } : {}),
// ...(supervisor_id !== undefined ? { supervisor_id } : {}), ...(supervisor_id !== undefined ? { supervisor_id } : {}),
// }, },
// include: { user: true }, 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 } }); const user = await this.prisma.users.findUnique({ where: { email } });
// if (!user) return null; if (!user) return null;
// // 2) Pas trouvé en actifs → regarder en archive (pour restauration) // 2) Pas trouvé en actifs → regarder en archive (pour restauration)
// const archived = await this.prisma.employeesArchive.findFirst({ // const archived = await this.prisma.employeesArchive.findFirst({
// where: { user_id: user.id }, // where: { user_id: user.id },
// include: { user: true }, // include: { user: true },
// }); // });
// if (archived) { // if (archived) {
// // Condition de restauration : last_work_day === null ou first_work_day fourni // // Condition de restauration : last_work_day === null ou first_work_day fourni
// const restore = dto.last_work_day === null || dto.first_work_day != null; // const restore = dto.last_work_day === null || dto.first_work_day != null;
// if (restore) { // if (restore) {
// return this.restoreEmployee(archived, dto); // return this.restoreEmployee(archived, dto);
// } // }
// } // }
// // 3) Ni actif, ni archivé → 404 dans le controller // 3) Ni actif, ni archivé → 404 dans le controller
// return null; return null;
// } }
// //transfers the employee to archive and then delete from employees table //transfers the employee to archive and then delete from employees table
// private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise<EmployeesArchive> { // private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise<EmployeesArchive> {
// const last_work_d = toDateOrNull(dto.last_work_day!); // const last_work_d = toDateOrNull(dto.last_work_day!);
// if (!last_work_d) throw new Error('invalide last_work_day for archive'); // if (!last_work_d) throw new Error('invalide last_work_day for archive');
// return this.prisma.$transaction(async transaction => { // return this.prisma.$transaction(async transaction => {
// //detach crew from supervisor if employee is a supervisor // //detach crew from supervisor if employee is a supervisor
// await transaction.employees.updateMany({ // await transaction.employees.updateMany({
// where: { supervisor_id: active.id }, // where: { supervisor_id: active.id },
// data: { supervisor_id: null }, // data: { supervisor_id: null },
// }) // })
// const archived = await transaction.employeesArchive.create({ // const archived = await transaction.employeesArchive.create({
// data: { // data: {
// employee_id: active.id, // employee_id: active.id,
// user_id: active.user_id, // user_id: active.user_id,
// first_name: active.user.first_name, // first_name: active.user.first_name,
// last_name: active.user.last_name, // last_name: active.user.last_name,
// company_code: active.company_code, // company_code: active.company_code,
// job_title: active.job_title, // job_title: active.job_title,
// first_work_day: active.first_work_day, // first_work_day: active.first_work_day,
// last_work_day: last_work_d, // last_work_day: last_work_d,
// supervisor_id: active.supervisor_id ?? null, // supervisor_id: active.supervisor_id ?? null,
// is_supervisor: active.is_supervisor, // is_supervisor: active.is_supervisor,
// external_payroll_id: active.external_payroll_id, // external_payroll_id: active.external_payroll_id,
// }, // },
// include: { user: true } // include: { user: true }
// }); // });
// //delete from employees table // //delete from employees table
// await transaction.employees.delete({ where: { id: active.id } }); // await transaction.employees.delete({ where: { id: active.id } });
// //return archived employee // //return archived employee
// return archived // return archived
// }); // });
// } // }
// //transfers the employee from archive to the employees table // //transfers the employee from archive to the employees table
// private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise<Employees> { // private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise<Employees> {
// // const first_work_d = toDateOrUndefined(dto.first_work_day); // // const first_work_d = toDateOrUndefined(dto.first_work_day);
// return this.prisma.$transaction(async transaction => { // return this.prisma.$transaction(async transaction => {
// //restores the archived employee into the employees table // //restores the archived employee into the employees table
// const restored = await transaction.employees.create({ // const restored = await transaction.employees.create({
// data: { // data: {
// user_id: archived.user_id, // user_id: archived.user_id,
// company_code: archived.company_code, // company_code: archived.company_code,
// job_title: archived.job_title, // job_title: archived.job_title,
// first_work_day: archived.first_work_day, // first_work_day: archived.first_work_day,
// last_work_day: null, // last_work_day: null,
// is_supervisor: archived.is_supervisor ?? false, // is_supervisor: archived.is_supervisor ?? false,
// external_payroll_id: archived.external_payroll_id, // external_payroll_id: archived.external_payroll_id,
// }, // },
// }); // });
// //deleting archived entry by id // //deleting archived entry by id
// await transaction.employeesArchive.delete({ where: { id: archived.id } }); // await transaction.employeesArchive.delete({ where: { id: archived.id } });
// //return restored employee // //return restored employee
// return restored; // return restored;
// }); // });
// } // }
// //fetches all archived employees // //fetches all archived employees
// async findAllArchived(): Promise<EmployeesArchive[]> { // async findAllArchived(): Promise<EmployeesArchive[]> {
// return this.prisma.employeesArchive.findMany(); // return this.prisma.employeesArchive.findMany();
// } // }
// //fetches an archived employee // //fetches an archived employee
// async findOneArchived(id: number): Promise<EmployeesArchive> { // async findOneArchived(id: number): Promise<EmployeesArchive> {
// return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } }); // return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } });
// } // }
// } }

View File

@ -1,230 +1,230 @@
// import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
// import { PrismaService } from 'src/prisma/prisma.service'; import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto";
// import { EmployeeListItemDto } from '../dtos/list-employee.dto'; import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto";
// import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto'; import { PrismaService } from "src/prisma/prisma.service";
// @Injectable() @Injectable()
// export class EmployeesService { export class EmployeesService {
// constructor(private readonly prisma: PrismaService) { } constructor(private readonly prisma: PrismaService) { }
// findListEmployees(): Promise<EmployeeListItemDto[]> { findListEmployees(): Promise<EmployeeListItemDto[]> {
// return this.prisma.employees.findMany({ return this.prisma.employees.findMany({
// select: { select: {
// user: { user: {
// select: { select: {
// first_name: true, first_name: true,
// last_name: true, last_name: true,
// email: true, email: true,
// }, },
// }, },
// supervisor: { supervisor: {
// select: { select: {
// user: { user: {
// select: { select: {
// first_name: true, first_name: true,
// last_name: true, last_name: true,
// }, },
// }, },
// }, },
// }, },
// job_title: true, job_title: true,
// company_code: true, company_code: true,
// } }
// }).then(rows => rows.map(r => ({ }).then(rows => rows.map(r => ({
// first_name: r.user.first_name, first_name: r.user.first_name,
// last_name: r.user.last_name, last_name: r.user.last_name,
// email: r.user.email, email: r.user.email,
// company_name: r.company_code, company_name: r.company_code,
// job_title: r.job_title, job_title: r.job_title,
// employee_full_name: `${r.user.first_name} ${r.user.last_name}`, 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, supervisor_full_name: r.supervisor ? `${r.supervisor.user.first_name} ${r.supervisor.user.last_name}` : null,
// })), })),
// ); );
// } }
// async findOneProfile(email: string): Promise<EmployeeProfileItemDto> { async findOneProfile(email: string): Promise<EmployeeProfileItemDto> {
// const emp = await this.prisma.employees.findFirst({ const emp = await this.prisma.employees.findFirst({
// where: { user: { email } }, where: { user: { email } },
// select: { select: {
// user: { user: {
// select: { select: {
// first_name: true, first_name: true,
// last_name: true, last_name: true,
// email: true, email: true,
// phone_number: true, phone_number: true,
// residence: true, residence: true,
// }, },
// }, },
// supervisor: { supervisor: {
// select: { select: {
// user: { user: {
// select: { select: {
// first_name: true, first_name: true,
// last_name: true, last_name: true,
// }, },
// }, },
// }, },
// }, },
// job_title: true, job_title: true,
// company_code: true, company_code: true,
// first_work_day: true, first_work_day: true,
// last_work_day: true, last_work_day: true,
// } }
// }); });
// if (!emp) throw new NotFoundException(`Employee with email ${email} not found`); if (!emp) throw new NotFoundException(`Employee with email ${email} not found`);
// return { return {
// first_name: emp.user.first_name, first_name: emp.user.first_name,
// last_name: emp.user.last_name, last_name: emp.user.last_name,
// email: emp.user.email, email: emp.user.email,
// residence: emp.user.residence, residence: emp.user.residence,
// phone_number: emp.user.phone_number, phone_number: emp.user.phone_number,
// company_name: emp.company_code, company_name: emp.company_code,
// job_title: emp.job_title, job_title: emp.job_title,
// employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`, employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`,
// first_work_day: emp.first_work_day.toISOString().slice(0, 10), 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, 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, 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<Employees> { // async create(dto: CreateEmployeeDto): Promise<Employees> {
// // const { // const {
// // first_name, // first_name,
// // last_name, // last_name,
// // email, // email,
// // phone_number, // phone_number,
// // residence, // residence,
// // external_payroll_id, // external_payroll_id,
// // company_code, // company_code,
// // job_title, // job_title,
// // first_work_day, // first_work_day,
// // last_work_day, // last_work_day,
// // is_supervisor, // is_supervisor,
// // } = dto; // } = dto;
// // return this.prisma.$transaction(async (transaction) => { // return this.prisma.$transaction(async (transaction) => {
// // const user: Users = await transaction.users.create({ // const user: Users = await transaction.users.create({
// // data: { // data: {
// // first_name, // first_name,
// // last_name, // last_name,
// // email, // email,
// // phone_number, // phone_number,
// // residence, // residence,
// // }, // },
// // }); // });
// // return transaction.employees.create({ // return transaction.employees.create({
// // data: { // data: {
// // user_id: user.id, // user_id: user.id,
// // external_payroll_id, // external_payroll_id,
// // company_code, // company_code,
// // job_title, // job_title,
// // first_work_day, // first_work_day,
// // last_work_day, // last_work_day,
// // is_supervisor, // is_supervisor,
// // }, // },
// // }); // });
// // }); // });
// // } // }
// // findAll(): Promise<Employees[]> { // findAll(): Promise<Employees[]> {
// // return this.prisma.employees.findMany({ // return this.prisma.employees.findMany({
// // include: { user: true }, // include: { user: true },
// // }); // });
// // } // }
// // async findOne(email: string): Promise<Employees> { // async findOne(email: string): Promise<Employees> {
// // const emp = await this.prisma.employees.findFirst({ // const emp = await this.prisma.employees.findFirst({
// // where: { user: { email } }, // where: { user: { email } },
// // include: { user: true }, // include: { user: true },
// // }); // });
// // //add search for archived employees // //add search for archived employees
// // if (!emp) { // if (!emp) {
// // throw new NotFoundException(`Employee with email: ${email} not found`); // throw new NotFoundException(`Employee with email: ${email} not found`);
// // } // }
// // return emp; // return emp;
// // } // }
// // async update( // async update(
// // email: string, // email: string,
// // dto: UpdateEmployeeDto, // dto: UpdateEmployeeDto,
// // ): Promise<Employees> { // ): Promise<Employees> {
// // const emp = await this.findOne(email); // const emp = await this.findOne(email);
// // const { // const {
// // first_name, // first_name,
// // last_name, // last_name,
// // phone_number, // phone_number,
// // residence, // residence,
// // external_payroll_id, // external_payroll_id,
// // company_code, // company_code,
// // job_title, // job_title,
// // first_work_day, // first_work_day,
// // last_work_day, // last_work_day,
// // is_supervisor, // is_supervisor,
// // email: new_email, // email: new_email,
// // } = dto; // } = dto;
// // return this.prisma.$transaction(async (transaction) => { // return this.prisma.$transaction(async (transaction) => {
// // if( // if(
// // first_name !== undefined || // first_name !== undefined ||
// // last_name !== undefined || // last_name !== undefined ||
// // new_email !== undefined || // new_email !== undefined ||
// // phone_number !== undefined || // phone_number !== undefined ||
// // residence !== undefined // residence !== undefined
// // ){ // ){
// // await transaction.users.update({ // await transaction.users.update({
// // where: { id: emp.user_id }, // where: { id: emp.user_id },
// // data: { // data: {
// // ...(first_name !== undefined && { first_name }), // ...(first_name !== undefined && { first_name }),
// // ...(last_name !== undefined && { last_name }), // ...(last_name !== undefined && { last_name }),
// // ...(email !== undefined && { email }), // ...(email !== undefined && { email }),
// // ...(phone_number !== undefined && { phone_number }), // ...(phone_number !== undefined && { phone_number }),
// // ...(residence !== undefined && { residence }), // ...(residence !== undefined && { residence }),
// // }, // },
// // }); // });
// // } // }
// // const updated = await transaction.employees.update({ // const updated = await transaction.employees.update({
// // where: { id: emp.id }, // where: { id: emp.id },
// // data: { // data: {
// // ...(external_payroll_id !== undefined && { external_payroll_id }), // ...(external_payroll_id !== undefined && { external_payroll_id }),
// // ...(company_code !== undefined && { company_code }), // ...(company_code !== undefined && { company_code }),
// // ...(first_work_day !== undefined && { first_work_day }), // ...(first_work_day !== undefined && { first_work_day }),
// // ...(last_work_day !== undefined && { last_work_day }), // ...(last_work_day !== undefined && { last_work_day }),
// // ...(job_title !== undefined && { job_title }), // ...(job_title !== undefined && { job_title }),
// // ...(is_supervisor !== undefined && { is_supervisor }), // ...(is_supervisor !== undefined && { is_supervisor }),
// // }, // },
// // }); // });
// // return updated; // return updated;
// // }); // });
// // } // }
// // async remove(email: string): Promise<Employees> { // async remove(email: string): Promise<Employees> {
// // const emp = await this.findOne(email); // const emp = await this.findOne(email);
// // return this.prisma.$transaction(async (transaction) => { // return this.prisma.$transaction(async (transaction) => {
// // await transaction.employees.updateMany({ // await transaction.employees.updateMany({
// // where: { supervisor_id: emp.id }, // where: { supervisor_id: emp.id },
// // data: { supervisor_id: null }, // data: { supervisor_id: null },
// // }); // });
// // const deleted_employee = await transaction.employees.delete({ // const deleted_employee = await transaction.employees.delete({
// // where: {id: emp.id }, // where: {id: emp.id },
// // }); // });
// // await transaction.users.delete({ // await transaction.users.delete({
// // where: { id: emp.user_id }, // where: { id: emp.user_id },
// // }); // });
// // return deleted_employee; // return deleted_employee;
// // }); // });
// // } // }
// } }

View File

@ -1,9 +1,9 @@
// export function toDateOrNull(v?: string | null): Date | null { export function toDateOrNull(v?: string | null): Date | null {
// if (!v) return null; if (!v) return null;
// const day = new Date(v); const day = new Date(v);
// return isNaN(day.getTime()) ? null : day; return isNaN(day.getTime()) ? null : day;
// } }
// export function toDateOrUndefined(v?: string | null): Date | undefined { export function toDateOrUndefined(v?: string | null): Date | undefined {
// const day = toDateOrNull(v ?? undefined); const day = toDateOrNull(v ?? undefined);
// return day === null ? undefined : day; return day === null ? undefined : day;
// } }

View File

@ -4,38 +4,38 @@ import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export abstract class AbstractUserService { export abstract class AbstractUserService {
constructor(protected readonly prisma: PrismaService) {} constructor(protected readonly prisma: PrismaService) { }
findAll(): Promise<Users[]> { findAll(): Promise<Users[]> {
return this.prisma.users.findMany(); return this.prisma.users.findMany();
} }
async findOne( id: string ): Promise<Users> { async findOne(id: string): Promise<Users> {
const user = await this.prisma.users.findUnique({ where: { id } }); const user = await this.prisma.users.findUnique({ where: { id } });
if (!user) { if (!user) {
throw new NotFoundException(`User #${id} not found`); throw new NotFoundException(`User #${id} not found`);
} }
return user; return user;
} }
async findOneByEmail( email: string ): Promise<Partial<Users>> { async findOneByEmail(email: string): Promise<Partial<Users>> {
const user = await this.prisma.users.findUnique({ where: { email } }); const user = await this.prisma.users.findUnique({ where: { email } });
if (!user) { if (!user) {
throw new NotFoundException(`No user with email #${email} exists`); throw new NotFoundException(`No user with email #${email} exists`);
} }
const clean_user = { const clean_user = {
first_name: user.first_name, first_name: user.first_name,
last_name: user.last_name, last_name: user.last_name,
email: user.email, email: user.email,
role: user.role, role: user.role,
} }
return clean_user;
}
async remove(id: string): Promise<Users> { return clean_user;
await this.findOne(id); }
return this.prisma.users.delete({ where: { id } });
} async remove(id: string): Promise<Users> {
await this.findOne(id);
return this.prisma.users.delete({ where: { id } });
}
} }

View File

@ -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 { 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 { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
@Controller('schedule-presets') @Controller('schedule-presets')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class SchedulePresetsController { export class SchedulePresetsController {
constructor( constructor(
private readonly upsertService: SchedulePresetsUpsertService, private readonly upsertService: SchedulePresetsUpsertService,
private readonly getService: SchedulePresetsGetService, private readonly getService: SchedulePresetsGetService,
private readonly applyPresetsService: SchedulePresetsApplyService, private readonly applyPresetsService: SchedulePresetsApplyService,
){} ) { }
//used to create a schedule preset //used to create a schedule preset
@Post('create') @Post('create')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) @RolesAllowed(...MANAGER_ROLES)
async createPreset( @Req() req, @Body() dto: SchedulePresetsDto ) { async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) {
const email = req.user?.email; const email = req.user?.email;
return await this.upsertService.createPreset(email, dto); return await this.upsertService.createPreset(email, dto);
} }
//used to update an already existing schedule preset //used to update an already existing schedule preset
@Patch('update/:preset_id') @Patch('update/:preset_id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) @RolesAllowed(...MANAGER_ROLES)
async updatePreset( @Param('preset_id', ParseIntPipe) preset_id: number,@Body() dto: SchedulePresetsUpdateDto ) { async updatePreset(@Param('preset_id', ParseIntPipe) preset_id: number, @Body() dto: SchedulePresetsUpdateDto) {
return await this.upsertService.updatePreset(preset_id, dto); return await this.upsertService.updatePreset(preset_id, dto);
} }
//used to delete a schedule preset //used to delete a schedule preset
@Delete('delete/:preset_id') @Delete('delete/:preset_id')
@RolesAllowed(RoleEnum.ADMIN) @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); return await this.upsertService.deletePreset(preset_id);
} }
//used to show the list of available schedule presets //used to show the list of available schedule presets
@Get('find-list') @Get('find-list')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) @RolesAllowed(...MANAGER_ROLES)
async findListById( @Req() req) { async findListById(@Req() req) {
const email = req.user?.email; const email = req.user?.email;
return this.getService.getSchedulePresets(email); return this.getService.getSchedulePresets(email);
} }
//used to apply a preset to a timesheet //used to apply a preset to a timesheet
@Post('apply-presets') @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; const email = req.user?.email;
if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); 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 (!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD');
return this.applyPresetsService.applyToTimesheet(email, preset_name, start_date); return this.applyPresetsService.applyToTimesheet(email, preset_name, start_date);
} }
} }

View File

@ -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 { 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 { 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 { 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 { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
@Controller('shift') @Controller('shift')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class ShiftController { export class ShiftController {
constructor( constructor(
private readonly upsert_service: ShiftsUpsertService, private readonly upsert_service: ShiftsUpsertService,
){} ){}
@Post('create') @Post('create')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise<CreateShiftResult[]> { createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
const email = req.user?.email; const email = req.user?.email;
const list = Array.isArray(dtos) ? dtos : []; const list = Array.isArray(dtos) ? dtos : [];
@ -21,10 +22,7 @@ export class ShiftController {
return this.upsert_service.createShifts(email, dtos) return this.upsert_service.createShifts(email, dtos)
} }
//change Body to receive dtos
@Patch('update') @Patch('update')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
updateBatch( @Body() dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]>{ updateBatch( @Body() dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]>{
const list = Array.isArray(dtos) ? dtos: []; const list = Array.isArray(dtos) ? dtos: [];
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)');
@ -32,9 +30,8 @@ export class ShiftController {
} }
@Delete(':shift_id') @Delete(':shift_id')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
remove(@Param('shift_id') shift_id: number ) { remove(@Param('shift_id') shift_id: number ) {
return this.upsert_service.deleteShift(shift_id); return this.upsert_service.deleteShift(shift_id);
} }
} }

View File

@ -41,16 +41,16 @@ export class ShiftsUpsertService {
if (normed.end_time <= normed.start_time) { if (normed.end_time <= normed.start_time) {
const error = { const error = {
error_code: 'SHIFT_OVERLAP', error_code: 'SHIFT_OVERLAP',
conflicts: { conflicts: {
start_time: toStringFromHHmm(normed.start_time), start_time: toStringFromHHmm(normed.start_time),
end_time: toStringFromHHmm(normed.end_time), end_time: toStringFromHHmm(normed.end_time),
date: toStringFromDate(normed.date), date: toStringFromDate(normed.date),
}, },
}; };
return { index, error }; return { index, error };
} }
if(!normed.end_time) throw new BadRequestException('A shift needs an end_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'); if (!normed.start_time) throw new BadRequestException('A shift needs a start_time');
const timesheet = await this.prisma.timesheets.findUnique({ const timesheet = await this.prisma.timesheets.findUnique({
where: { id: dto.timesheet_id, employee_id }, where: { id: dto.timesheet_id, employee_id },
@ -59,9 +59,9 @@ export class ShiftsUpsertService {
if (!timesheet) { if (!timesheet) {
const error = { const error = {
error_code: 'INVALID_TIMESHEET', error_code: 'INVALID_TIMESHEET',
conflicts: { conflicts: {
start_time: toStringFromHHmm(normed.start_time), start_time: toStringFromHHmm(normed.start_time),
end_time: toStringFromHHmm(normed.end_time), end_time: toStringFromHHmm(normed.end_time),
date: toStringFromDate(normed.date), date: toStringFromDate(normed.date),
}, },
}; };
@ -116,14 +116,14 @@ export class ShiftsUpsertService {
if ( if (
overlaps( overlaps(
{ start: ordered[j - 1].start, end: ordered[j - 1].end, date: ordered[j - 1].date }, { 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({ const error = new ConflictException({
error_code: 'SHIFT_OVERLAP', error_code: 'SHIFT_OVERLAP',
conflicts: { conflicts: {
start_time: toStringFromHHmm(ordered[j].start), start_time: toStringFromHHmm(ordered[j].start),
end_time: toStringFromHHmm(ordered[j].end), end_time: toStringFromHHmm(ordered[j].end),
date: toStringFromDate(ordered[j].date), date: toStringFromDate(ordered[j].date),
}, },
}); });
@ -148,7 +148,7 @@ export class ShiftsUpsertService {
where: { timesheet_id, date: day_date }, where: { timesheet_id, date: day_date },
select: { start_time: true, end_time: true, id: true, date: true }, 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) => { normed_shifts.forEach((x, i) => {
@ -164,8 +164,8 @@ export class ShiftsUpsertService {
existing = []; existing = [];
existing_map.set(map_key, existing); existing_map.set(map_key, existing);
} }
const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.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})); { start: normed.start_time, end: normed.end_time, date: normed.date }));
if (hit) { if (hit) {
results[index] = { results[index] = {
ok: false, ok: false,
@ -201,7 +201,7 @@ export class ShiftsUpsertService {
}; };
existing.push(normalized_row); existing.push(normalized_row);
existing_map.set(map_key, existing); existing_map.set(map_key, existing);
const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id);
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx);
@ -236,12 +236,11 @@ export class ShiftsUpsertService {
// recalculate overtime after update // recalculate overtime after update
// return an updated version to display // return an updated version to display
async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> { async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> {
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 updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => {
const { shift_id, ...rest } = item; const { shift_id, ...rest } = item;
console.log('id received: ', shift_id); if (!shift_id) throw new BadRequestException({ error_code: 'SHIFT_INVALID' });
if (!Number.isInteger(shift_id)) throw new ConflictException({ error_code: 'INVALID_SHIFT'});
const changes: UpdateShiftChanges = {}; const changes: UpdateShiftChanges = {};
if (rest.date !== undefined) changes.date = rest.date; if (rest.date !== undefined) changes.date = rest.date;
@ -266,13 +265,15 @@ export class ShiftsUpsertService {
const existing = regroup_id.get(update.shift_id); const existing = regroup_id.get(update.shift_id);
if (!existing) { if (!existing) {
return updates.map(exist => exist.shift_id === update.shift_id return updates.map(exist => exist.shift_id === update.shift_id
? ({ ok: false, id: update.shift_id, error: new NotFoundException(`Shift with id: ${update.shift_id} not found`) } as UpdateShiftResult) ? ({ ok: false, id: update.shift_id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult)
: ({ ok: false, id: exist.shift_id, error: new BadRequestException('Batch aborted due to missing shift') })); : ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
);
} }
if (existing.is_approved) { if (existing.is_approved) {
return updates.map(exist => exist.shift_id === update.shift_id return updates.map(exist => exist.shift_id === update.shift_id
? ({ ok: false, id: update.shift_id, error: new BadRequestException('Approved shift cannot be updated') } as UpdateShiftResult) ? ({ ok: false, id: update.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult)
: ({ ok: false, id: exist.shift_id, error: new BadRequestException('Batch aborted due to approved shift in update set') })); : ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
);
} }
} }
@ -308,12 +309,14 @@ export class ShiftsUpsertService {
where: { timesheet_id: group.timesheet_id, date: day_date }, where: { timesheet_id: group.timesheet_id, date: day_date },
select: { id: true, start_time: true, end_time: true, date: true }, select: { id: true, start_time: true, end_time: true, date: true },
}); });
groups.set(key(group.timesheet_id, day_date), { existing: existing.map(row => ({ groups.set(key(group.timesheet_id, day_date), {
id: row.id, existing: existing.map(row => ({
start: row.start_time, id: row.id,
end: row.end_time, start: row.start_time,
date: row.date, end: row.end_time,
})), incoming: planned_updates }); date: row.date,
})), incoming: planned_updates
});
} }
for (const planned of planned_updates) { for (const planned of planned_updates) {
@ -321,7 +324,7 @@ export class ShiftsUpsertService {
const group = groups.get(keys)!; const group = groups.get(keys)!;
const conflict = group.existing.find(row => 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 }) { start: planned.normed.start_time, end: planned.normed.end_time, date: planned.normed.date })
); );
if (conflict) { if (conflict) {
@ -330,9 +333,9 @@ export class ShiftsUpsertService {
? ({ ? ({
ok: false, id: exist.shift_id, error:{ ok: false, id: exist.shift_id, error:{
error_code: 'SHIFT_OVERLAP', error_code: 'SHIFT_OVERLAP',
conflicts: { conflicts: {
start_time: toStringFromHHmm(conflict.start), start_time: toStringFromHHmm(conflict.start),
end_time: toStringFromHHmm(conflict.end), end_time: toStringFromHHmm(conflict.end),
date: toStringFromDate(conflict.date), date: toStringFromDate(conflict.date),
}, },
} }
@ -346,29 +349,29 @@ export class ShiftsUpsertService {
for (const planned of planned_updates) { for (const planned of planned_updates) {
const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); const keys = key(planned.exist_shift.timesheet_id, planned.normed.date);
if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []); if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []);
regoup_by_day.get(keys)!.push({ regoup_by_day.get(keys)!.push({
id: planned.exist_shift.id, id: planned.exist_shift.id,
start: planned.normed.start_time, start: planned.normed.start_time,
end: planned.normed.end_time, end: planned.normed.end_time,
date: planned.normed.date date: planned.normed.date
}); });
} }
for (const arr of regoup_by_day.values()) { for (const arr of regoup_by_day.values()) {
arr.sort((a, b) => a.start.getTime() - b.start.getTime()); arr.sort((a, b) => a.start.getTime() - b.start.getTime());
for (let i = 1; i < arr.length; i++) { for (let i = 1; i < arr.length; i++) {
if (overlaps( if (overlaps(
{ start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].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 }) { start: arr[i].start, end: arr[i].end, date: arr[i].date })
) { ) {
const error = { const error = {
error_code: 'SHIFT_OVERLAP', error_code: 'SHIFT_OVERLAP',
conflicts: { conflicts: {
start_time: toStringFromHHmm(arr[i].start), start_time: toStringFromHHmm(arr[i].start),
end_time: toStringFromHHmm(arr[i].end), end_time: toStringFromHHmm(arr[i].end),
date: toStringFromDate(arr[i].date), date: toStringFromDate(arr[i].date),
}, },
}; };
return updates.map(exist => ({ ok: false, id: exist.shift_id, error: error })); return updates.map(exist => ({ ok: false, id: exist.shift_id, error: error }));
} }
@ -427,7 +430,7 @@ export class ShiftsUpsertService {
where: { id: shift_id }, where: { id: shift_id },
select: { id: true, date: true, timesheet_id: true }, 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 } }); await tx.shifts.delete({ where: { id: shift_id } });

View File

@ -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 { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; 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 { 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') @Controller('timesheets')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class TimesheetController { export class TimesheetController {
constructor( constructor(
private readonly timesheetOverview: GetTimesheetsOverviewService, private readonly timesheetOverview: GetTimesheetsOverviewService,
private readonly approvalService: TimesheetApprovalService, private readonly approvalService: TimesheetApprovalService,
){} ) { }
@Get() @Get()
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) getTimesheetByPayPeriod(
async getTimesheetByIds( @Req() req,
@Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) { @Query('year', ParseIntPipe) year: number,
@Query('period_number', ParseIntPipe) period_number: number
) {
const email = req.user?.email; 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); return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number);
} }
@Patch('timesheet-approval') @Patch('timesheet-approval')
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) @RolesAllowed(...MANAGER_ROLES)
async approveTimesheet( approveTimesheet(
@Body('timesheet_id', ParseIntPipe) timesheet_id: number, @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); return this.approvalService.approveTimesheetById(timesheet_id, is_approved);
} }