refactor(EsLint): EsLint corrections

This commit is contained in:
Matthieu Haineault 2026-02-27 10:09:24 -05:00
parent 37a4da7923
commit aa72651a67
79 changed files with 401 additions and 702 deletions

View File

@ -1,6 +1,6 @@
// @ts-check // @ts-check
import eslint from '@eslint/js'; import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; // import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals'; import globals from 'globals';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
@ -10,7 +10,7 @@ export default tseslint.config(
}, },
eslint.configs.recommended, eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended, // eslintPluginPrettierRecommended,
{ {
languageOptions: { languageOptions: {
globals: { globals: {

View File

@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common'; import { Controller } from '@nestjs/common';
@Controller() @Controller()
export class AppController { } export class AppController { }

View File

@ -1,7 +1,6 @@
import { BadRequestException, Module, ValidationPipe } from '@nestjs/common'; import { BadRequestException, Module, ValidationPipe } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { NotificationsModule } from './shared/notifications/notifications.module';
import { PrismaPostgresModule } from '../prisma/postgres/prisma-postgres.module'; import { PrismaPostgresModule } from '../prisma/postgres/prisma-postgres.module';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
@ -21,7 +20,6 @@ import { CustomerSupportModule } from 'src/customer-support/customer-support.mod
AuthenticationModule, AuthenticationModule,
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
ScheduleModule.forRoot(), //cronjobs ScheduleModule.forRoot(), //cronjobs
NotificationsModule,
PrismaPostgresModule, PrismaPostgresModule,
PrismaMariadbModule, PrismaMariadbModule,
PrismaLegacyModule, PrismaLegacyModule,

View File

@ -8,26 +8,29 @@ import { Modules as ModulesEnum } from "prisma/postgres/generated/prisma/client/
@Controller('chatbot') @Controller('chatbot')
export class ChatbotController { export class ChatbotController {
constructor(private readonly chatbotService: ChatbotService) {} constructor(private readonly chatbotService: ChatbotService) { }
@Post('') @Post('')
@ModuleAccessAllowed(ModulesEnum.chatbot) @ModuleAccessAllowed(ModulesEnum.chatbot)
async testConnection(@Body() body: UserMessageDto, @Access('email') email: string): Promise<Message> { async testConnection(
return await this.chatbotService.pingExternalApi(body, email); @Body() body: UserMessageDto,
} @Access('email') email: string,
): Promise<Message> {
return await this.chatbotService.pingExternalApi(body, email);
}
// @Post('context') // @Post('context')
// @ModuleAccessAllowed(ModulesEnum.chatbot) // @ModuleAccessAllowed(ModulesEnum.chatbot)
// async sendContext(@Body() body: PageContextDto): Promise<string> { // async sendContext(@Body() body: PageContextDto): Promise<string> {
// const sendPageContext = await this.chatbotService.sendPageContext(body); // const sendPageContext = await this.chatbotService.sendPageContext(body);
// return sendPageContext; // return sendPageContext;
// } // }
// Will have to modify later on to accomodate newer versions of User Auth/User type Structure // Will have to modify later on to accomodate newer versions of User Auth/User type Structure
// @Post('user') // @Post('user')
// @ModuleAccessAllowed(ModulesEnum.chatbot) // @ModuleAccessAllowed(ModulesEnum.chatbot)
// async sendUserCredentials(@Access('email') email: string,): Promise<boolean> { // async sendUserCredentials(@Access('email') email: string,): Promise<boolean> {
// const sendUserContext = await this.chatbotService.sendUserContext(email); // const sendUserContext = await this.chatbotService.sendUserContext(email);
// return sendUserContext; // return sendUserContext;
// } // }
} }

View File

@ -4,9 +4,14 @@ import { HttpModule } from '@nestjs/axios';
import { ChatbotService } from 'src/chatbot/chatbot.service'; import { ChatbotService } from 'src/chatbot/chatbot.service';
@Module({ @Module({
imports: [HttpModule], imports: [
controllers: [ChatbotController], HttpModule,
providers: [ChatbotService], ],
exports: [], controllers: [
ChatbotController,
],
providers: [
ChatbotService,
],
}) })
export class ChatbotModule {} export class ChatbotModule { }

View File

@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto';
import { HttpService } from '@nestjs/axios'; import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { PageContextDto } from 'src/chatbot/dtos/page-context.dto'; import { ChatbotResponseDto, UserMessageDto } from 'src/chatbot/dtos/user-message.dto';
import { Message } from 'src/chatbot/dtos/dialog-message.dto'; import { Message } from 'src/chatbot/dtos/dialog-message.dto';
@Injectable() @Injectable()
@ -14,41 +13,11 @@ export class ChatbotService {
const { data } = await firstValueFrom(this.httpService.post( const { data } = await firstValueFrom(this.httpService.post(
'https://n8nai.targo.ca/webhook/chatty-Mcbot', 'https://n8nai.targo.ca/webhook/chatty-Mcbot',
{ userInput: body.userInput, userId: email, sessionId: this.sessionId, pageContext: body.pageContext ?? undefined } { userInput: body.userInput, userId: email, sessionId: this.sessionId, pageContext: body.pageContext ?? undefined }
)); )) as ChatbotResponseDto;
return { return {
text: data[0].output, text: data[0].output,
sent: false, sent: false,
}; };
} }
// async sendPageContext(body: PageContextDto, email: string) {
// const { data } = await firstValueFrom(
// this.httpService.post(
// 'https://n8nai.targo.ca/webhook/chatty-Mcbot',
// { features: body, userId: email, userInput: '' },
// ),
// );
// return data;
// }
// Will have to modify later on to accomodate newer versions of User Auth/User type Structure
// async sendUserContext(user_email: string) {
// if (!this.sessionId) {
// this.sessionId = 'SessionId = ' + user_email;
// }
// const response = await firstValueFrom(
// this.httpService.post(
// 'https://n8nai.targo.ca/webhook/chatty-Mcbot',
// {
// userId: this.sessionId,
// userInput: '',
// features: '',
// },
// { headers: { 'Content-Tyoe': 'application/json' } },
// ),
// );
// return response.data;
// }
} }

View File

@ -1,9 +1,6 @@
import { IsBoolean, IsString } from 'class-validator'; import { IsBoolean, IsString } from 'class-validator';
export class Message { export class Message {
@IsString() @IsString() text: string;
text!: string; @IsBoolean() sent: boolean;
@IsBoolean()
sent!: boolean;
} }

View File

@ -1,15 +1,8 @@
import { IsArray, IsString } from 'class-validator'; import { IsArray, IsOptional, IsString } from 'class-validator';
export class PageContextDto { export class PageContextDto {
@IsString() @IsString() name: string;
name: string; @IsString() description: string;
@IsArray() features: string[];
@IsString() @IsString() @IsOptional() path?: string;
description: string;
@IsArray()
features: string[];
@IsString()
path?: string;
} }

View File

@ -1,11 +1,18 @@
import { Transform, Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { PageContextDto } from './page-context.dto'; import { PageContextDto } from './page-context.dto';
export class UserMessageDto { export class UserMessageDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Transform(({ value }) => value.trim()) @IsString() userInput: string;
userInput!: string; @IsOptional() @Type(() => PageContextDto) pageContext?: PageContextDto | undefined;
@IsOptional() @Type(() => PageContextDto) pageContext?: PageContextDto | undefined; }
export class ChatbotResponseDto {
@Type(() => ChatbotOutput) data: ChatbotOutput[];
}
export class ChatbotOutput {
@IsString() output: string;
} }

View File

@ -1,9 +1,14 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common"; import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { UserDto } from "src/identity-and-account/users-management/user.dto";
export const Access = createParamDecorator( export interface AuthenticatedRequest extends Request {
(data:string, ctx: ExecutionContext) => { user: UserDto;
const request = ctx.switchToHttp().getRequest(); }
const user = request.user;
export const Access = createParamDecorator<keyof UserDto | undefined>(
(data, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
const user: UserDto = request.user;
return data ? user?.[data] : user; return data ? user?.[data] : user;
}, },
); );

View File

@ -7,13 +7,7 @@ import {
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { MODULES_KEY } from '../decorators/modules-guard.decorators'; import { MODULES_KEY } from '../decorators/modules-guard.decorators';
import { Modules } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Modules } from "prisma/postgres/generated/prisma/client/postgres/client";
import { AuthenticatedRequest } from 'src/common/decorators/module-access.decorators';
interface RequestWithUser extends Request {
// TODO: Create an actual user model based on OAuth signin
user: any;
}
@Injectable() @Injectable()
export class ModulesGuard implements CanActivate { export class ModulesGuard implements CanActivate {
@ -27,18 +21,18 @@ export class ModulesGuard implements CanActivate {
if (!requiredModules || requiredModules.length === 0) { if (!requiredModules || requiredModules.length === 0) {
return true; return true;
} }
const request = ctx.switchToHttp().getRequest<RequestWithUser>(); const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
const user = request.user; const user = request.user;
if (!user) { if (!user) {
return false; return false;
} }
for (const module of requiredModules) { for (const module of requiredModules) {
if (!user.user_module_access.includes(module)) { if (!user.user_module_access.includes(module)) {
throw new ForbiddenException( throw new ForbiddenException(
`This account does not have required access to: ${module}. current user modules: ${user.user_module_access} , required modules: ${requiredModules}`, `This account does not have required access to: ${module}. current user modules: ${user.user_module_access.toString()} , required modules: ${requiredModules.toString()}`,
); );
} }
} }
return true; return true;
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client";
import { Result } from "src/common/errors/result-error.factory";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Result } from "src/common/errors/result-error.factory";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
@ -25,7 +25,9 @@ export class BankCodesResolver {
}; };
//finds only id by type //finds only id by type
readonly findBankCodeIDByType = async (type: string, client?: Tx readonly findBankCodeIDByType = async (
type: string,
client?: Tx
): Promise<Result<number, string>> => { ): Promise<Result<number, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const bank_code = await db.bankCodes.findFirst({ const bank_code = await db.bankCodes.findFirst({
@ -37,7 +39,9 @@ export class BankCodesResolver {
return { success: true, data: bank_code.id }; return { success: true, data: bank_code.id };
} }
readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx readonly findTypeByBankCodeId = async (
bank_code_id: number,
client?: Tx
): Promise<Result<string, string>> => { ): Promise<Result<string, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const bank_code = await db.bankCodes.findFirst({ const bank_code = await db.bankCodes.findFirst({

View File

@ -1,8 +1,8 @@
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client";
import { Result } from "src/common/errors/result-error.factory";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
@ -12,7 +12,9 @@ export class EmailToIdResolver {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
// find employee_id using email // find employee_id using email
readonly findIdByEmail = async (email: string, client?: Tx readonly findIdByEmail = async (
email: string,
client?: Tx
): Promise<Result<number, string>> => { ): Promise<Result<number, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const employee = await db.employees.findFirst({ const employee = await db.employees.findFirst({
@ -24,7 +26,9 @@ export class EmailToIdResolver {
} }
// find user_id using email // find user_id using email
readonly resolveUserIdWithEmail = async (email: string, client?: Tx readonly resolveUserIdWithEmail = async (
email: string,
client?: Tx
): Promise<Result<string, string>> => { ): Promise<Result<string, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const user = await db.users.findFirst({ const user = await db.users.findFirst({
@ -34,6 +38,4 @@ export class EmailToIdResolver {
if (!user) return { success: false, error: `EMPLOYEE_NOT_FOUND` }; if (!user) return { success: false, error: `EMPLOYEE_NOT_FOUND` };
return { success: true, data: user.id }; return { success: true, data: user.id };
} }
readonly findFullNameByEmail
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Prisma, PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/client";
import { Result } from "src/common/errors/result-error.factory";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
@ -9,7 +9,10 @@ type Tx = Prisma.TransactionClient | PrismaClient;
export class FullNameResolver { export class FullNameResolver {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<Result<string, string>> => { readonly resolveFullName = async (
employee_id: number,
client?: Tx
): Promise<Result<string, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const employee = await db.employees.findUnique({ const employee = await db.employees.findUnique({
where: { id: employee_id }, where: { id: employee_id },

View File

@ -19,7 +19,9 @@ interface ShiftKey {
export class ShiftIdResolver { export class ShiftIdResolver {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
readonly findShiftIdByData = async (key: ShiftKey, client?: Tx readonly findShiftIdByData = async (
key: ShiftKey,
client?: Tx
): Promise<Result<number, string>> => { ): Promise<Result<number, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const shift = await db.shifts.findFirst({ const shift = await db.shifts.findFirst({

View File

@ -1,8 +1,8 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { EmailToIdResolver } from "./email-id.mapper"; import { EmailToIdResolver } from "./email-id.mapper";
import { Result } from "src/common/errors/result-error.factory";
import { weekStartSunday } from "src/common/utils/date-utils"; import { weekStartSunday } from "src/common/utils/date-utils";
import { Result } from "src/common/errors/result-error.factory";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/internal/class"; import { PrismaClient } from "prisma/postgres/generated/prisma/client/postgres/internal/class";
import { Prisma } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Prisma } from "prisma/postgres/generated/prisma/client/postgres/client";
@ -16,7 +16,11 @@ export class EmployeeTimesheetResolver {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
) { } ) { }
readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise<Result<{ id: number }, string>> => { readonly findTimesheetIdByEmail = async (
email: string,
date: Date,
client?: Tx
): Promise<Result<{ id: number }, string>> => {
const db = (client ?? this.prisma) as PrismaClient; const db = (client ?? this.prisma) as PrismaClient;
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if (!employee_id.success) return { success: false, error: employee_id.error } if (!employee_id.success) return { success: false, error: employee_id.error }

View File

@ -20,7 +20,10 @@ export abstract class BaseApprovalService<T> {
protected abstract delegateFor(tx: TransactionClient): UpdatableDelegate<T>; protected abstract delegateFor(tx: TransactionClient): UpdatableDelegate<T>;
//standard update Aproval //standard update Aproval
async updateApproval(id: number, is_approved: boolean): Promise<T> { async updateApproval(
id: number,
is_approved: boolean
): Promise<T> {
try { try {
return await this.delegate.update({ return await this.delegate.update({
where: { id }, where: { id },
@ -34,7 +37,11 @@ export abstract class BaseApprovalService<T> {
} }
} }
async updateApprovalWithTransaction(tx: TransactionClient, id: number, is_approved: boolean): Promise<T> { async updateApprovalWithTransaction(
tx: TransactionClient,
id: number,
is_approved: boolean
): Promise<T> {
try { try {
return await this.delegateFor(tx).update({ return await this.delegateFor(tx).update({
where: { id }, where: { id },

View File

@ -1,21 +0,0 @@
//Prisma 'where' clause for DTO filters
export function buildPrismaWhere<T extends Record<string, any>>(dto: T): Record <string, any> {
const where: Record<string,any> = {};
for (const [key,value] of Object.entries(dto)) {
if (value === undefined || value === null) continue;
if (key.endsWith('_contains')) {
const field = key.slice(0, - '_contains'.length);
where[field] = { constains: value };
} else if (key === 'start_date' || key === 'end_date') {
where.date = where.date || {};
const op = key === 'start_date' ? 'gte' : 'lte';
where.date[op] = new Date(value);
} else {
where[key] = value;
}
}
return where;
}

View File

@ -1,38 +0,0 @@
// //This file is used to store function that help translate MariaDB data to Typescript manipulation requirements for the type "boolean".
// import { PhoneAddrEnhancedCapable } from "src/customer-support/dtos/phone.dto";
// //From MariaDB to Frontend
// export const fromTinyIntToBoolean = async (tinyInt: number): Promise<boolean> => {
// let booleanValue = true;
// if ((tinyInt = 0) || (tinyInt = -1)) return booleanValue = false;
// return booleanValue;
// }
// //From Frontend to MariaDB TinyInt boolean 1 - 0
// export const fromBooleanToTinyInt = async (boolean: boolean): Promise<number> => {
// return boolean ? 1 : 0;
// }
// //From Frontend to MariaDB TinyInt boolean -1 - 1
// export const fromBooleanToTinyIntNegative = async (boolean: boolean): Promise<number> => {
// return boolean ? 1 : -1;
// }
// //From MariaDB to Frontend String boolean yes - no / Y - N / etc...
// export const fromStringToBoolean = async (string: string): Promise<boolean> => {
// let booleanValue = true;
// let stringValue = string.toLowerCase();
// if ((stringValue = "n") || (stringValue = "no") || (stringValue = "non")) {
// return booleanValue = false;
// }
// return booleanValue;
// }
// export const fromBooleanToEnum = async (boolean: boolean): Promise<PhoneAddrEnhancedCapable> => {
// return boolean ? PhoneAddrEnhancedCapable.Y : PhoneAddrEnhancedCapable.N;
// }
// export const fromEnumToBoolean = async (enumValue: PhoneAddrEnhancedCapable): Promise<boolean> => {
// return enumValue ? true : false;
// }

View File

@ -1 +0,0 @@
//This file is used to store function that help translate MariaDB data to Typescript manipulation requirements for the type "number".

View File

@ -1 +0,0 @@
//This file is used to store function that help translate MariaDB data to Typescript manipulation requirements for the type "string".

View File

@ -9,17 +9,23 @@ import { UsersService } from 'src/identity-and-account/users-management/services
@Module({ @Module({
imports: [ PassportModule.register({ imports: [
session: true, PassportModule.register({
defaultStrategy: 'openidconnect' session: true,
}), UsersModule, ], defaultStrategy: 'openidconnect'
}), UsersModule,
],
providers: [ providers: [
AuthentikAuthService, AuthentikAuthService,
AuthentikStrategy, AuthentikStrategy,
ExpressSessionSerializer, ExpressSessionSerializer,
UsersService, UsersService,
], ],
exports: [ AuthentikAuthService ], exports: [
controllers: [AuthController], AuthentikAuthService
],
controllers: [
AuthController
],
}) })
export class AuthenticationModule {} export class AuthenticationModule { }

View File

@ -16,7 +16,7 @@ export class AuthController {
@Get('callback') @Get('callback')
@UseGuards(OIDCLoginGuard) @UseGuards(OIDCLoginGuard)
loginCallback(@Req() req: Request, @Res() res: Response) { loginCallback(@Req() _req: Request, @Res() res: Response) {
res.redirect(process.env.REDIRECT_URL_DEV!); res.redirect(process.env.REDIRECT_URL_DEV!);
} }

View File

@ -1,11 +1,12 @@
import { ExecutionContext, Injectable } from '@nestjs/common'; import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
@Injectable() @Injectable()
export class OIDCLoginGuard extends AuthGuard('openidconnect') { export class OIDCLoginGuard extends AuthGuard('openidconnect') {
async canActivate(context: ExecutionContext) { async canActivate(context: ExecutionContext) {
const result = (await super.canActivate(context)) as boolean; const result = (await super.canActivate(context)) as boolean;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest<Request>();
await super.logIn(request); await super.logIn(request);
return result; return result;
} }

View File

@ -9,7 +9,7 @@ export class ExpressSessionSerializer extends PassportSerializer {
} }
done(null, user); done(null, user);
} }
deserializeUser(payload: any, done: (err: any, payload: string) => void): any { deserializeUser(payload: any, done: (err: any, payload: any) => void): any {
if (!payload){ if (!payload){
done(new UnauthorizedException('Deserialize user error'), payload); done(new UnauthorizedException('Deserialize user error'), payload);
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/identity-and-account/users-management/services/users.service'; import { UsersService } from 'src/identity-and-account/users-management/services/users.service';
import { UserDto } from 'src/identity-and-account/users-management/user.dto';
@Injectable() @Injectable()
export class AuthentikAuthService { export class AuthentikAuthService {
constructor(private usersService: UsersService) {} constructor(private usersService: UsersService) {}
async validateUser(user_email: string): Promise<any> { async validateUser(user_email: string): Promise<Partial<UserDto>> {
const user = await this.usersService.findOneByEmail(user_email); const user = await this.usersService.findOneByEmail(user_email);
return user; return user;

View File

@ -39,9 +39,9 @@ export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidcon
async validate( async validate(
_issuer: string, _issuer: string,
profile: Profile, _profile: Profile,
_context: any, _context: any,
_idToken: string, idToken: string,
_accessToken: string, _accessToken: string,
_refreshToken: string, _refreshToken: string,
_params: any, _params: any,
@ -50,9 +50,9 @@ export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidcon
try { try {
const components = _idToken.split('.'); const components = idToken.split('.');
const payload = Buffer.from(components[1], "base64").toString('utf-8'); const payload = Buffer.from(components[1], "base64").toString('utf-8');
const claims = JSON.parse(payload); const claims = JSON.parse(payload) as AuthentikPayload;
if (!claims.email) return cb(new Error('Missing email in OIDC profile'), false); if (!claims.email) return cb(new Error('Missing email in OIDC profile'), false);

View File

@ -18,13 +18,17 @@ export class EmployeesController {
@Get('personal-profile') @Get('personal-profile')
@ModuleAccessAllowed(ModulesEnum.personal_profile) @ModuleAccessAllowed(ModulesEnum.personal_profile)
async findOwnProfile(@Access('email') email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> { async findOwnProfile(
@Access('email') email: string
): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
return await this.getService.findOwnProfile(email); return await this.getService.findOwnProfile(email);
} }
@Get('profile') @Get('profile')
@ModuleAccessAllowed(ModulesEnum.personal_profile) @ModuleAccessAllowed(ModulesEnum.personal_profile)
async findProfile(@Access('email') email: string, @Query('employee_email') employee_email?: string, async findProfile(
@Access('email') email: string,
@Query('employee_email') employee_email?: string,
): Promise<Result<Partial<EmployeeDetailedDto>, string>> { ): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
return await this.getService.findOneDetailedProfile(email, employee_email); return await this.getService.findOneDetailedProfile(email, employee_email);
} }
@ -37,13 +41,17 @@ export class EmployeesController {
@Post('create') @Post('create')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async createEmployee(@Body() dto: EmployeeDetailedUpsertDto): Promise<Result<boolean, string>> { async createEmployee(
@Body() dto: EmployeeDetailedUpsertDto
): Promise<Result<boolean, string>> {
return await this.createService.createEmployee(dto); return await this.createService.createEmployee(dto);
} }
@Patch('update') @Patch('update')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async updateEmployee(@Body() dto:EmployeeDetailedUpsertDto){ async updateEmployee(
@Body() dto:EmployeeDetailedUpsertDto
){
return await this.updateService.updateEmployee(dto); return await this.updateService.updateEmployee(dto);
} }
} }

View File

@ -7,8 +7,9 @@ import { EmployeesUpdateService } from 'src/identity-and-account/employees/servi
import { EmployeesCreateService } from 'src/identity-and-account/employees/services/employees-create.service'; import { EmployeesCreateService } from 'src/identity-and-account/employees/services/employees-create.service';
@Module({ @Module({
imports: [], controllers: [
controllers: [EmployeesController], EmployeesController
],
providers: [ providers: [
EmployeesGetService, EmployeesGetService,
EmployeesUpdateService, EmployeesUpdateService,
@ -16,6 +17,8 @@ import { EmployeesCreateService } from 'src/identity-and-account/employees/servi
AccessGetService, AccessGetService,
EmailToIdResolver EmailToIdResolver
], ],
exports: [EmployeesGetService], exports: [
EmployeesGetService
],
}) })
export class EmployeesModule { } export class EmployeesModule { }

View File

@ -10,7 +10,9 @@ export class HomePageController {
@Get('help') @Get('help')
@ModuleAccessAllowed(ModulesEnum.dashboard) @ModuleAccessAllowed(ModulesEnum.dashboard)
async getIntroductionHelper(@Access('email') email: string) { async getIntroductionHelper(
@Access('email') email: string
) {
return await this.homePageService.buildHomePageHelpMessage(email); return await this.homePageService.buildHomePageHelpMessage(email);
} }
} }

View File

@ -4,8 +4,15 @@ import { HomePageController } from "src/identity-and-account/help/help-page.cont
import { HomePageService } from "src/identity-and-account/help/help-page.service"; import { HomePageService } from "src/identity-and-account/help/help-page.service";
@Module({ @Module({
controllers: [HomePageController], controllers: [
providers: [HomePageService, EmailToIdResolver], HomePageController
exports: [HomePageService], ],
providers: [
HomePageService,
EmailToIdResolver
],
exports: [
HomePageService
],
}) })
export class HomePageModule { }; export class HomePageModule { };

View File

@ -10,7 +10,9 @@ export class HomePageService {
private readonly emailresolver: EmailToIdResolver, private readonly emailresolver: EmailToIdResolver,
) { } ) { }
buildHomePageHelpMessage = async (email: string): Promise<Result<string[], string>> => { buildHomePageHelpMessage = async (
email: string
): Promise<Result<string[], string>> => {
const user_id = await this.emailresolver.resolveUserIdWithEmail(email); const user_id = await this.emailresolver.resolveUserIdWithEmail(email);
if (!user_id.success) return { success: false, error: 'INVALID_EMAIL' }; if (!user_id.success) return { success: false, error: 'INVALID_EMAIL' };

View File

@ -12,14 +12,19 @@ export class PreferencesController {
@Patch('update') @Patch('update')
@ModuleAccessAllowed(ModulesEnum.personal_profile) @ModuleAccessAllowed(ModulesEnum.personal_profile)
async updatePreferences(@Access('email') email: string, @Body() payload: PreferencesDto async updatePreferences(
@Access('email') email: string,
@Body() payload: PreferencesDto
): Promise<Result<PreferencesDto, string>> { ): Promise<Result<PreferencesDto, string>> {
return this.service.updatePreferences(email, payload); return this.service.updatePreferences(email, payload);
} }
@Get() @Get()
@ModuleAccessAllowed(ModulesEnum.personal_profile) @ModuleAccessAllowed(ModulesEnum.personal_profile)
async findPreferences(@Access('email') email: string, @Query() employee_email?: string) { async findPreferences(
@Access('email') email: string,
@Query() employee_email?: string
) {
return this.service.findPreferences(email, employee_email); return this.service.findPreferences(email, employee_email);
} }

View File

@ -4,9 +4,16 @@ import { PreferencesService } from "./preferences.service";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
@Module({ @Module({
controllers: [ PreferencesController ], controllers: [
providers: [ PreferencesService, EmailToIdResolver ], PreferencesController
exports: [ PreferencesService ], ],
providers: [
PreferencesService,
EmailToIdResolver
],
exports: [
PreferencesService
],
}) })
export class PreferencesModule {} export class PreferencesModule {}

View File

@ -11,7 +11,10 @@ export class PreferencesService {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
) { } ) { }
async findPreferences(email: string, employee_email?: string): Promise<Result<PreferencesDto, string>> { async findPreferences(
email: string,
employee_email?: string
): Promise<Result<PreferencesDto, string>> {
const account_email = employee_email ?? email; const account_email = employee_email ?? email;
const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
@ -42,7 +45,10 @@ export class PreferencesService {
return { success: true, data: preferences }; return { success: true, data: preferences };
} }
async updatePreferences(email: string, dto: PreferencesDto): Promise<Result<PreferencesDto, string>> { async updatePreferences(
email: string,
dto: PreferencesDto
): Promise<Result<PreferencesDto, string>> {
const user_id = await this.emailResolver.resolveUserIdWithEmail(email); const user_id = await this.emailResolver.resolveUserIdWithEmail(email);
if (!user_id.success) return { success: false, error: user_id.error } if (!user_id.success) return { success: false, error: user_id.error }

View File

@ -1,10 +1,10 @@
import { IsBoolean } from "class-validator"; import { IsBoolean } from "class-validator";
export class ModuleAccess { export class ModuleAccess {
@IsBoolean() timesheets!: boolean; @IsBoolean() timesheets: boolean;
@IsBoolean() timesheets_approval!: boolean; @IsBoolean() timesheets_approval: boolean;
@IsBoolean() employee_list!: boolean; @IsBoolean() employee_list: boolean;
@IsBoolean() employee_management!: boolean; @IsBoolean() employee_management: boolean;
@IsBoolean() personal_profile!: boolean; @IsBoolean() personal_profile: boolean;
@IsBoolean() dashboard!: boolean; @IsBoolean() dashboard: boolean;
} }

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, Query, Req } from "@nestjs/common"; import { Body, Controller, Get, Patch, Query } from "@nestjs/common";
import { Access } from "src/common/decorators/module-access.decorators"; import { Access } from "src/common/decorators/module-access.decorators";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { ModuleAccess } from "src/identity-and-account/user-module-access/module-acces.dto"; import { ModuleAccess } from "src/identity-and-account/user-module-access/module-acces.dto";
@ -16,7 +16,9 @@ export class ModuleAccessController {
@Get() @Get()
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async findAccess(@Access('email') email: string, @Query('employee_email') employee_email?: string async findAccess(
@Access('email') email: string,
@Query('employee_email') employee_email?: string
): Promise<Result<boolean, string>> { ): Promise<Result<boolean, string>> {
await this.getService.findModuleAccess(email, employee_email); await this.getService.findModuleAccess(email, employee_email);
return { success: true, data: true }; return { success: true, data: true };
@ -24,7 +26,10 @@ export class ModuleAccessController {
@Patch('update') @Patch('update')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async updateAccess(@Access('email') email: string, @Body() dto: ModuleAccess, @Query('employee_email') employee_email?: string async updateAccess(
@Access('email') email: string,
@Body() dto: ModuleAccess,
@Query('employee_email') employee_email?: string
): Promise<Result<boolean, string>> { ): Promise<Result<boolean, string>> {
await this.updateService.updateModuleAccess(email, dto, employee_email); await this.updateService.updateModuleAccess(email, dto, employee_email);
return { success: true, data: true }; return { success: true, data: true };

View File

@ -5,8 +5,16 @@ import { AccessGetService } from "src/identity-and-account/user-module-access/se
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
@Module({ @Module({
controllers: [ModuleAccessController], controllers: [
providers: [AccessUpdateService, AccessGetService, EmailToIdResolver], ModuleAccessController
exports: [AccessGetService], ],
providers: [
AccessUpdateService,
AccessGetService,
EmailToIdResolver
],
exports: [
AccessGetService
],
}) })
export class ModuleAccessModule { } export class ModuleAccessModule { }

View File

@ -12,7 +12,10 @@ export class AccessGetService {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
) { } ) { }
async findModuleAccess(email: string, employee_email?: string): Promise<Result<ModuleAccess, string>> { async findModuleAccess(
email: string,
employee_email?: string
): Promise<Result<ModuleAccess, string>> {
const account_email = employee_email ?? email; const account_email = employee_email ?? email;
const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };

View File

@ -11,7 +11,11 @@ export class AccessUpdateService {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
) { } ) { }
async updateModuleAccess(email: string, dto: ModuleAccess, employee_email?: string): Promise<Result<ModuleAccess, string>> { async updateModuleAccess(
email: string,
dto: ModuleAccess,
employee_email?: string
): Promise<Result<ModuleAccess, string>> {
const account_email = employee_email ?? email; const account_email = employee_email ?? email;
const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
@ -52,7 +56,10 @@ export class AccessUpdateService {
return { success: true, data: updated_access }; return { success: true, data: updated_access };
} }
async revokeModuleAccess(email: string, employee_email?: string): Promise<Result<ModuleAccess, string>> { async revokeModuleAccess(
email: string,
employee_email?: string
): Promise<Result<ModuleAccess, string>> {
const account_email = employee_email ?? email; const account_email = employee_email ?? email;
const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };

View File

@ -7,7 +7,9 @@ import { PrismaPostgresService } from 'prisma/postgres/prisma-postgres.service';
export abstract class AbstractUserService { export abstract class AbstractUserService {
constructor(protected readonly prisma: PrismaPostgresService) { } constructor(protected readonly prisma: PrismaPostgresService) { }
async findOneByEmail(email: string): Promise<Partial<Users>> { async findOneByEmail(
email: string
): Promise<Partial<Users>> {
const user = await this.prisma.users.findUnique({ const user = await this.prisma.users.findUnique({
where: { email }, where: { email },
include: { include: {

View File

@ -6,5 +6,5 @@ export class UserDto {
@IsString() last_name: string; @IsString() last_name: string;
@IsEmail() email: string; @IsEmail() email: string;
@IsEnum(Roles) role: string; @IsEnum(Roles) role: string;
@IsArray() @IsEnum(Modules, { each: true }) user_module_access!: Modules[]; @IsArray() @IsEnum(Modules, { each: true }) user_module_access: Modules[];
} }

View File

@ -3,8 +3,14 @@ import { UsersService } from './services/users.service';
import { PrismaPostgresModule } from 'prisma/postgres/prisma-postgres.module'; import { PrismaPostgresModule } from 'prisma/postgres/prisma-postgres.module';
@Module({ @Module({
imports: [PrismaPostgresModule], imports: [
providers: [UsersService], PrismaPostgresModule
exports: [UsersService], ],
providers: [
UsersService
],
exports: [
UsersService
],
}) })
export class UsersModule {} export class UsersModule {}

View File

@ -1,8 +1,8 @@
import 'reflect-metadata'; import 'reflect-metadata';
import * as nodeCrypto from 'crypto'; // import * as nodeCrypto from 'crypto';
if (!(globalThis as any).crypto) { // if (!(globalThis as any).crypto) {
(globalThis as any).crypto = nodeCrypto; // (globalThis as any).crypto = nodeCrypto;
} // }
import { NestFactory, Reflector } from '@nestjs/core'; import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ModulesGuard } from './common/guards/modules.guard'; import { ModulesGuard } from './common/guards/modules.guard';
@ -14,7 +14,7 @@ import { PrismaPostgresService } from 'prisma/postgres/prisma-postgres.service';
const SESSION_TOKEN_DURATION_MINUTES = 180 const SESSION_TOKEN_DURATION_MINUTES = 180
async function bootstrap() { async function bootstrap() {
(BigInt.prototype as any).toJSON = function () { return Number(this) }; BigInt.prototype['toJSON'] = function () { return Number(this) };
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const prisma_postgres = app.get(PrismaPostgresService); const prisma_postgres = app.get(PrismaPostgresService);

View File

@ -1,34 +0,0 @@
// import { Module } from "@nestjs/common";
// import { ScheduleModule } from "@nestjs/schedule";
// import { TimesheetsModule } from "../timesheets/timesheets.module";
// import { ExpensesModule } from "../expenses/expenses.module";
// import { ShiftsModule } from "../shifts/shifts.module";
// import { LeaveRequestsModule } from "../leave-requests/leave-requests.module";
// import { ArchivalService } from "./services/archival.service";
// import { EmployeesArchiveController } from "./controllers/employees-archive.controller";
// import { ExpensesArchiveController } from "./controllers/expenses-archive.controller";
// import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller";
// import { ShiftsArchiveController } from "./controllers/shifts-archive.controller";
// import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller";
// import { EmployeesModule } from "../employees/employees.module";
// @Module({
// imports: [
// EmployeesModule,
// ScheduleModule,
// TimesheetsModule,
// ExpensesModule,
// ShiftsModule,
// LeaveRequestsModule,
// ],
// providers: [ArchivalService],
// controllers: [
// EmployeesArchiveController,
// ExpensesArchiveController,
// LeaveRequestsArchiveController,
// ShiftsArchiveController,
// TimesheetsArchiveController,
// ],
// })
// export class ArchivalModule {}

View File

@ -1,38 +0,0 @@
// import { TimesheetArchiveService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service";
// import { ExpensesArchivalService } from "src/time-and-attendance/modules/expenses/services/expenses-archival.service";
// import { ShiftsArchivalService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service";
// import { Injectable, Logger } from "@nestjs/common";
// import { Cron } from "@nestjs/schedule";
// @Injectable()
// export class ArchivalService {
// private readonly logger = new Logger(ArchivalService.name);
// constructor(
// private readonly timesheetsService: TimesheetArchiveService,
// private readonly expensesService: ExpensesArchivalService,
// private readonly shiftsService: ShiftsArchivalService,
// ) {}
// @Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
// async handleMonthlyArchival() {
// const today = new Date();
// const dayOfMonth = today.getDate();
// if (dayOfMonth > 7) {
// this.logger.warn('Archive {awaiting 1st monday of the month for archivation process}')
// return;
// }
// this.logger.log('monthly archivation in process');
// try {
// await this.timesheetsService.archiveOld();
// await this.expensesService.archiveOld();
// await this.shiftsService.archiveOld();
// // await this.leaveRequestsService.archiveExpired();
// this.logger.log('archivation process done');
// } catch (err) {
// this.logger.error('an error occured during archivation process ', err);
// }
// }
// }

View File

@ -1,32 +0,0 @@
// import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from "@nestjs/common";
// import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
// import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { EmployeesArchivalService } from "src/modules/employees/services/employees-archival.service";
// @ApiTags('Employee Archives')
// // @UseGuards()
// @Controller('archives/employees')
// export class EmployeesArchiveController {
// constructor(private readonly employeesArchiveService: EmployeesArchivalService) {}
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'List of archived employees'})
// @ApiResponse({ status: 200, description: 'List of archived employees', isArray: true })
// async findAllArchived(): Promise<EmployeesArchive[]> {
// return this.employeesArchiveService.findAllArchived();
// }
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR,RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Fetch employee in archives with its Id'})
// @ApiResponse({ status: 200, description: 'Archived employee found'})
// async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<EmployeesArchive> {
// try{
// return await this.employeesArchiveService.findOneArchived(id);
// }catch {
// throw new NotFoundException(`Archived employee #${id} not found`);
// }
// }
// }

View File

@ -1,31 +0,0 @@
// import { UseGuards, Controller, Get, Param, ParseIntPipe, NotFoundException } from "@nestjs/common";
// import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
// import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { ExpensesArchivalService } from "src/time-and-attendance/modules/expenses/services/expenses-archival.service";
// @ApiTags('Expense Archives')
// // @UseGuards()
// @Controller('archives/expenses')
// export class ExpensesArchiveController {
// constructor(private readonly expensesService: ExpensesArchivalService) {}
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'List of archived expenses'})
// @ApiResponse({ status: 200, description: 'List of archived expenses', isArray: true })
// async findAllArchived(): Promise<ExpensesArchive[]> {
// return this.expensesService.findAllArchived();
// }
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Fetch expense in archives with its Id'})
// @ApiResponse({ status: 200, description: 'Archived expense found'})
// async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<ExpensesArchive> {
// try{
// return await this.expensesService.findOneArchived(id);
// }catch {
// throw new NotFoundException(`Archived expense #${id} not found`);
// }
// }
// }

View File

@ -1,7 +0,0 @@
// import { Controller } from '@nestjs/common';
// import { ApiTags } from '@nestjs/swagger';
// @ApiTags('LeaveRequests Archives')
// // @UseGuards()
// @Controller('archives/leaveRequests')
// export class LeaveRequestsArchiveController {}

View File

@ -1,31 +0,0 @@
// import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common";
// import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
// import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { ShiftsArchivalService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service";
// @ApiTags('Shift Archives')
// // @UseGuards()
// @Controller('archives/shifts')
// export class ShiftsArchiveController {
// constructor(private readonly shiftsService: ShiftsArchivalService) {}
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'List of archived shifts'})
// @ApiResponse({ status: 200, description: 'List of archived shifts', isArray: true })
// async findAllArchived(): Promise<ShiftsArchive[]> {
// return this.shiftsService.findAllArchived();
// }
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR,RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Fetch shift in archives with its Id'})
// @ApiResponse({ status: 200, description: 'Archived shift found'})
// async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<ShiftsArchive> {
// try{
// return await this.shiftsService.findOneArchived(id);
// }catch {
// throw new NotFoundException(`Archived shift #${id} not found`);
// }
// }
// }

View File

@ -1,32 +0,0 @@
// import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from "@nestjs/common";
// import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
// import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { TimesheetArchiveService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service";
// @ApiTags('Timesheet Archives')
// // @UseGuards()
// @Controller('archives/timesheets')
// export class TimesheetsArchiveController {
// constructor(private readonly timesheetsService: TimesheetArchiveService) {}
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'List of archived timesheets'})
// @ApiResponse({ status: 200, description: 'List of archived timesheets', isArray: true })
// async findAllArchived(): Promise<TimesheetsArchive[]> {
// return this.timesheetsService.findAllArchived();
// }
// @Get()
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Fetch timesheet in archives with its Id'})
// @ApiResponse({ status: 200, description: 'Archived timesheet found'})
// async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<TimesheetsArchive> {
// try{
// return await this.timesheetsService.findOneArchived(id);
// }catch {
// throw new NotFoundException(`Archived timesheet #${id} not found`);
// }
// }
// }

View File

@ -1,4 +0,0 @@
export const NOTIF_TYPES = {
SHIFT_OVERTIME_DAILY: 'shift.overtime.daily',
} as const;

View File

@ -1,9 +0,0 @@
export type NotificationCard = {
type: string;
message: string;
severity?: 'info'|'warn'|'error';
icon?: string;
link?: string;
meta?: Record<string, any>
ts: string; //new Date().toISOString()
};

View File

@ -1,21 +0,0 @@
import { Controller, Get, Req, Sse,
MessageEvent as NestMessageEvent } from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from 'rxjs/operators';
import { NotificationsService } from "src/shared/notifications/notifications.service";
@Controller('notifications')
export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {}
@Get('summary')
async summary(@Req() req) {
return this.notificationsService.summary(String(req.user.id));
}
@Sse('stream')
stream(@Req() req): Observable<NestMessageEvent> {
const userId = String(req.user.id);
return this.notificationsService.stream(userId).pipe(map((data): NestMessageEvent => ({ data })))
}
}

View File

@ -1,9 +0,0 @@
import { Module } from "@nestjs/common";
import { NotificationsService } from "./notifications.service";
import { NotificationsController } from "src/shared/notifications/notifications.controller";
@Module({
providers: [NotificationsService],
controllers: [NotificationsController],
exports: [NotificationsService],
})
export class NotificationsModule {}

View File

@ -1,62 +0,0 @@
import { Injectable, Logger } from "@nestjs/common";
import { Subject } from "rxjs";
import { NotificationCard } from "./notification.types";
@Injectable()
export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
//Server-Sent Events FLUX and a buffer per user
private streams = new Map<string, Subject<NotificationCard>>();
private buffers = new Map<string, NotificationCard[]>();
private readonly BUFFER_MAX = Number(process.env.NOTIF_BUFFER_MAX ?? 50);
private getOrCreateStream(user_id: string): Subject<NotificationCard> {
let stream = this.streams.get(user_id);
if (!stream){
stream = new Subject<NotificationCard>();
this.streams.set(user_id, stream);
}
return stream;
}
private getOrCreateBuffer(user_id: string){
let buffer = this.buffers.get(user_id);
if(!buffer) {
buffer = [];
this.buffers.set(user_id, buffer);
}
return buffer;
}
//in-app pushes and keep a small history
notify(user_id: string, card: NotificationCard) {
const buffer = this.getOrCreateBuffer(user_id);
buffer.unshift(card);
if (buffer.length > this.BUFFER_MAX) {
buffer.length = this.BUFFER_MAX;
}
this.getOrCreateStream(user_id).next(card);
this.logger.debug(`Notification in-app => user: ${user_id} (${card.type})`);
}
//SSE flux for current user
stream(user_id: string) {
return this.getOrCreateStream(user_id).asObservable();
}
//return a summary of notifications kept in memory
async summary(user_id: string): Promise<NotificationCard[]> {
return this.getOrCreateBuffer(user_id);
}
//clear buffers from memory
clear(user_id: string) {
this.buffers.set(user_id, []);
}
onModuleDestroy() {
for (const stream of this.streams.values()) stream.complete();
this.streams.clear();
this.buffers.clear();
}
}

View File

@ -8,12 +8,10 @@ import { BankCodesService } from "src/time-and-attendance/bank-codes/bank-codes.
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
export class BankCodesControllers { export class BankCodesControllers {
constructor(private readonly bankCodesService: BankCodesService) { } constructor(private readonly bankCodesService: BankCodesService) { }
//_____________________________________________________________________________________________
// Deprecated or unused methods
//_____________________________________________________________________________________________
@Post() @Post()
create(@Body() dto: Prisma.BankCodesCreateInput create(
@Body() dto: Prisma.BankCodesCreateInput
): Promise<Result<boolean, string>> { ): Promise<Result<boolean, string>> {
return this.bankCodesService.create(dto); return this.bankCodesService.create(dto);
} }
@ -24,7 +22,9 @@ export class BankCodesControllers {
} }
@Patch(':id') @Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() dto: Prisma.BankCodesUpdateInput update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: Prisma.BankCodesUpdateInput
): Promise<Result<boolean, string>> { ): Promise<Result<boolean, string>> {
return this.bankCodesService.update(id, dto) return this.bankCodesService.update(id, dto)
} }

View File

@ -4,8 +4,13 @@ import { BankCodesControllers } from "src/time-and-attendance/bank-codes/bank-co
import { BankCodesService } from "src/time-and-attendance/bank-codes/bank-codes.service"; import { BankCodesService } from "src/time-and-attendance/bank-codes/bank-codes.service";
@Module({ @Module({
controllers: [BankCodesControllers], controllers: [
providers: [BankCodesService, PrismaPostgresService], BankCodesControllers
],
providers: [
BankCodesService,
PrismaPostgresService
],
}) })
export class BankCodesModule {} export class BankCodesModule {}

View File

@ -7,7 +7,9 @@ import { Result } from "src/common/errors/result-error.factory";
export class BankCodesService { export class BankCodesService {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
async create(dto: Prisma.BankCodesCreateInput): Promise<Result<boolean, string>> { async create(
dto: Prisma.BankCodesCreateInput
): Promise<Result<boolean, string>> {
try { try {
await this.prisma.bankCodes.create({ await this.prisma.bankCodes.create({
data: { data: {
@ -27,7 +29,10 @@ export class BankCodesService {
return this.prisma.bankCodes.findMany(); return this.prisma.bankCodes.findMany();
} }
async update(id: number, dto: Prisma.BankCodesUpdateInput): Promise<Result<boolean, string>> { async update(
id: number,
dto: Prisma.BankCodesUpdateInput
): Promise<Result<boolean, string>> {
try { try {
await this.prisma.bankCodes.update({ await this.prisma.bankCodes.update({
where: { id }, where: { id },
@ -44,7 +49,9 @@ export class BankCodesService {
} }
} }
async delete(id: number): Promise<Result<boolean, string>> { async delete(
id: number
): Promise<Result<boolean, string>> {
try { try {
await this.prisma.bankCodes.delete({ await this.prisma.bankCodes.delete({
where: { id }, where: { id },

View File

@ -9,7 +9,6 @@ import { Module } from "@nestjs/common";
@Module({ @Module({
imports:[],
providers: [ providers: [
HolidayService, HolidayService,
MileageService, MileageService,

View File

@ -8,7 +8,11 @@ export class BankedHoursService {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
//manage shifts with bank_code.type BANKING //manage shifts with bank_code.type BANKING
manageBankingHours = async (employee_id: number, asked_hours: number, type: string): Promise<Result<number, string>> => { manageBankingHours = async (
employee_id: number,
asked_hours: number,
type: string
): Promise<Result<number, string>> => {
if (asked_hours <= 0) return { success: false, error: 'INVALID_BANKING_HOURS' }; if (asked_hours <= 0) return { success: false, error: 'INVALID_BANKING_HOURS' };
try { try {
@ -50,8 +54,6 @@ export class BankedHoursService {
} else if (type === 'WITHDRAW_BANKED') { } else if (type === 'WITHDRAW_BANKED') {
if (asked_hours > banked_hours) { if (asked_hours > banked_hours) {
return { success: true, data: banked_hours } as Result<number, string>; return { success: true, data: banked_hours } as Result<number, string>;
} else {
} }
await tx.paidTimeOff.update({ await tx.paidTimeOff.update({
where: { employee_id: employee.id }, where: { employee_id: employee.id },
@ -69,7 +71,8 @@ export class BankedHoursService {
return result; return result;
} catch (error) { } catch (error) {
return { success: false, error: 'INVALID_BANKING_SHIFT' }; console.error(error);
return { success: false, error: 'INVALID_BANKING_SHIFT: ' };
} }
} }
} }

View File

@ -24,7 +24,11 @@ export class HolidayService {
* *
* @returns Average daily hours worked in the past four weeks as a `number`, to a maximum of `8` * @returns Average daily hours worked in the past four weeks as a `number`, to a maximum of `8`
*/ */
private async computeHoursPrevious4Weeks(external_payroll_id: number, company_code: number, holiday_date: Date): Promise<Result<number, string>> { private async computeHoursPrevious4Weeks(
external_payroll_id: number,
company_code: number,
holiday_date: Date
): Promise<Result<number, string>> {
try { try {
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720']; const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720'];
const holiday_week_start = getWeekStart(holiday_date); const holiday_week_start = getWeekStart(holiday_date);
@ -42,7 +46,7 @@ export class HolidayService {
}); });
if (!employee) if (!employee)
return {success: false, error: 'EMPLOYEE_NOT_FOUND'}; return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { where: {
@ -76,11 +80,16 @@ export class HolidayService {
const average_daily_hours = capped_total / 20; const average_daily_hours = capped_total / 20;
return { success: true, data: average_daily_hours }; return { success: true, data: average_daily_hours };
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: `an error occureded during holiday calculation` } return { success: false, error: `an error occureded during holiday calculation` }
} }
} }
async calculateHolidayPay(employeePayrollID: number, companyCode: number, holiday_date: Date): Promise<Result<number, string>> { async calculateHolidayPay(
employeePayrollID: number,
companyCode: number,
holiday_date: Date
): Promise<Result<number, string>> {
const average_daily_hours = await this.computeHoursPrevious4Weeks(employeePayrollID, companyCode, holiday_date); const average_daily_hours = await this.computeHoursPrevious4Weeks(employeePayrollID, companyCode, holiday_date);
if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error }; if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error };

View File

@ -7,7 +7,10 @@ export class MileageService {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
public async calculateReimbursement(amount: number, bank_code_id: number): Promise<Result<number, string>> { public async calculateReimbursement(
amount: number,
bank_code_id: number
): Promise<Result<number, string>> {
if (amount < 0) return { success: false, error: 'The amount must be higher than 0' }; if (amount < 0) return { success: false, error: 'The amount must be higher than 0' };
//fetch modifier //fetch modifier

View File

@ -27,11 +27,21 @@ type WeekOvertimeSummary = {
@Injectable() @Injectable()
export class OvertimeService { export class OvertimeService {
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING', 'OVERTIME', 'REGULAR', 'HOLIDAY', 'BANKING'] as const; // included types for weekly overtime calculation private INCLUDED_TYPES = [
'EMERGENCY',
'EVENING',
'OVERTIME',
'REGULAR',
'HOLIDAY',
'BANKING'
] as const;
constructor(private prisma: PrismaPostgresService) { } constructor(private prisma: PrismaPostgresService) { }
async getWeekOvertimeSummary(timesheet_id: number, date: Date, tx?: Tx): Promise<Result<WeekOvertimeSummary, string>> { async getWeekOvertimeSummary(
timesheet_id: number,
date: Date,
tx?: Tx
): Promise<Result<WeekOvertimeSummary, string>> {
const db = (tx ?? this.prisma) as PrismaClient; const db = (tx ?? this.prisma) as PrismaClient;
const week_start = getWeekStart(date); const week_start = getWeekStart(date);

View File

@ -8,7 +8,9 @@ import { Result } from "src/common/errors/result-error.factory";
export class SickLeaveService { export class SickLeaveService {
constructor(private readonly prisma: PrismaPostgresService) { } constructor(private readonly prisma: PrismaPostgresService) { }
async updateSickLeaveHours(employee_id: number): Promise<Result<boolean, string>> { async updateSickLeaveHours(
employee_id: number
): Promise<Result<boolean, string>> {
const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30;
const FOURTEEN_DAYS = 1000 * 60 * 60 * 24 * 14; const FOURTEEN_DAYS = 1000 * 60 * 60 * 24 * 14;
const today = new Date(); const today = new Date();
@ -51,7 +53,7 @@ export class SickLeaveService {
if (today.getTime() - employee.first_work_day.getTime() >= THIRTY_DAYS && employee.first_work_day.toISOString() === pto_details?.last_updated?.toISOString()) { if (today.getTime() - employee.first_work_day.getTime() >= THIRTY_DAYS && employee.first_work_day.toISOString() === pto_details?.last_updated?.toISOString()) {
const updated_pto = await this.addHoursToPTO(3 * 8, employee.id, today); const updated_pto = await this.addHoursToPTO(3 * 8, employee.id, today);
if (!updated_pto.success) return { success: updated_pto.success, error: updated_pto.error } if (!updated_pto.success) return { success: updated_pto.success, error: '' + updated_pto.error }
} }
const year_difference = today.getFullYear() - (pto_details!.last_updated?.getFullYear() ?? today.getFullYear()); const year_difference = today.getFullYear() - (pto_details!.last_updated?.getFullYear() ?? today.getFullYear());
@ -69,7 +71,10 @@ export class SickLeaveService {
} }
// create a new PTO row // create a new PTO row
async createNewPTORow(employee_id: number, today: Date): Promise<Result<Prisma.Result<typeof this.prisma.paidTimeOff, Prisma.PaidTimeOffDefaultArgs, 'findUnique' | 'create'>, string>> { async createNewPTORow(
employee_id: number,
today: Date
): Promise<Result<Prisma.Result<typeof this.prisma.paidTimeOff, Prisma.PaidTimeOffDefaultArgs, 'findUnique' | 'create'>, string>> {
try { try {
const new_pto_entry = await this.prisma.paidTimeOff.create({ const new_pto_entry = await this.prisma.paidTimeOff.create({
data: { data: {
@ -88,12 +93,16 @@ export class SickLeaveService {
return { success: true, data: new_pto_entry }; return { success: true, data: new_pto_entry };
} catch (error) { } catch (error) {
return { success: false, error }; return { success: false, error: '' + error };
} }
} }
// add n number of sick PTO hours to employee PTO // add n number of sick PTO hours to employee PTO
async addHoursToPTO(sick_hours: number, employee_id: number, last_updated: Date) { async addHoursToPTO(
sick_hours: number,
employee_id: number,
last_updated: Date
) {
try { try {
const update_pto = await this.prisma.paidTimeOff.update({ const update_pto = await this.prisma.paidTimeOff.update({
where: { where: {
@ -107,11 +116,15 @@ export class SickLeaveService {
return { success: true, data: update_pto }; return { success: true, data: update_pto };
} catch (error) { } catch (error) {
return { success: false, error }; console.error(error);
return { success: false, error: ''};
} }
}; };
takeSickLeaveHours = async (employee_id: number, asked_hours: number): Promise<Result<number, string>> => { takeSickLeaveHours = async (
employee_id: number,
asked_hours: number
): Promise<Result<number, string>> => {
if (asked_hours <= 0) return { success: false, error: 'INVALID_BANKING_HOURS' }; if (asked_hours <= 0) return { success: false, error: 'INVALID_BANKING_HOURS' };
try { try {
@ -154,74 +167,9 @@ export class SickLeaveService {
return result; return result;
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: 'INVALID_BANKING_SHIFT' }; return { success: false, error: 'INVALID_BANKING_SHIFT' };
} }
} }
//LEAVE REQUEST FUNCTION - DEPRECATED
// async calculateSickLeavePay(
// employee_id: number,
// reference_date: Date,
// days_requested: number,
// hours_per_day: number,
// modifier: number,
// ): Promise<Result<number, string>> {
// if (days_requested <= 0 || hours_per_day <= 0 || modifier <= 0) {
// return { success: true, data: 0 };
// }
// //sets the year to jan 1st to dec 31st
// const period_start = getYearStart(reference_date);
// const period_end = reference_date;
// //fetches all shifts of a selected employee
// const shifts = await this.prisma.shifts.findMany({
// where: {
// timesheet: { employee_id: employee_id },
// date: { gte: period_start, lte: period_end },
// },
// select: { date: true },
// });
// //count the amount of worked days
// const worked_dates = new Set(
// shifts.map((shift) => shift.date.toISOString().slice(0, 10)),
// );
// const days_worked = worked_dates.size;
// //less than 30 worked days returns 0
// if (days_worked < 30) {
// return { success: true, data: 0 };
// }
// //default 3 days allowed after 30 worked days
// let acquired_days = 3;
// //identify the date of the 30th worked day
// const ordered_dates = Array.from(worked_dates).sort();
// const threshold_date = new Date(ordered_dates[29]); // index 29 is the 30th day
// //calculate each completed month, starting the 1st of the next month
// const first_bonus_date = new Date(
// threshold_date.getFullYear(),
// threshold_date.getMonth() + 1,
// 1,
// );
// let months =
// (period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 +
// (period_end.getMonth() - first_bonus_date.getMonth()) +
// 1;
// if (months < 0) months = 0;
// acquired_days += months;
// //cap of 10 days
// if (acquired_days > 10) acquired_days = 10;
// const payable_days = Math.min(acquired_days, days_requested);
// const raw_hours = payable_days * hours_per_day * modifier;
// const rounded = roundToQuarterHour(raw_hours);
// return { success: true, data: rounded };
// }
} }

View File

@ -10,7 +10,6 @@ export class VacationService {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
) { } ) { }
//switch employeeId for email
async calculateVacationPay(email: string, start_date: Date, days_requested: number, modifier: number): Promise<Result<number, string>> { async calculateVacationPay(email: string, start_date: Date, days_requested: number, modifier: number): Promise<Result<number, string>> {
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if (!employee_id.success) return { success: false, error: employee_id.error } if (!employee_id.success) return { success: false, error: employee_id.error }
@ -20,7 +19,7 @@ export class VacationService {
where: { id: employee_id.data }, where: { id: employee_id.data },
select: { first_work_day: true }, select: { first_work_day: true },
}); });
if (!employee) return { success: false, error: `Employee #${employee_id} not found` } if (!employee) return { success: false, error: `Employee #${employee_id.data} not found` }
const hire_date = employee.first_work_day; const hire_date = employee.first_work_day;
@ -110,10 +109,8 @@ export class VacationService {
}); });
return result; return result;
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: 'INVALID_VACATION_SHIFT' } return { success: false, error: 'INVALID_VACATION_SHIFT' }
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException, Query } from "@nestjs/common"; import { Controller, Post, Param, Body, Patch, Delete, UnauthorizedException, Query } from "@nestjs/common";
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
@ -18,7 +18,10 @@ export class ExpenseController {
@Post('create') @Post('create')
@ModuleAccessAllowed(ModulesEnum.timesheets) @ModuleAccessAllowed(ModulesEnum.timesheets)
create(@Access('email') email: string, @Body() dto: ExpenseDto): Promise<Result<ExpenseDto, string>> { create(
@Access('email') email: string,
@Body() dto: ExpenseDto
): Promise<Result<ExpenseDto, string>> {
if (!email) throw new UnauthorizedException('Unauthorized User'); if (!email) throw new UnauthorizedException('Unauthorized User');
return this.createService.createExpense(dto, email); return this.createService.createExpense(dto, email);
} }

View File

@ -3,19 +3,6 @@ import { toDateFromString } from "src/common/utils/date-utils";
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
//makes sure that a string cannot exceed 280 chars
export const truncate280 = (input: string): string => {
return input.length > 280 ? input.slice(0, 280) : input;
}
//makes sure that the type of data of numeric values is valid
export const parseOptionalNumber = (value: unknown, field: string) => {
if (value == null) return undefined;
const parsed = Number(value);
if (Number.isNaN(parsed)) throw new Error(`Invalid value : ${value} for ${field}`);
return parsed;
};
//makes sure that comments are the right length the date is of Date type //makes sure that comments are the right length the date is of Date type
export const normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Result<NormalizedExpense, string>> => { export const normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Result<NormalizedExpense, string>> => {
const mileage = parseOptionalNumber(dto.mileage, "mileage"); const mileage = parseOptionalNumber(dto.mileage, "mileage");
@ -38,3 +25,16 @@ export const normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Resu
} }
}; };
} }
//makes sure that a string cannot exceed 280 chars
export const truncate280 = (input: string): string => {
return input.length > 280 ? input.slice(0, 280) : input;
}
//makes sure that the type of data of numeric values is valid
export const parseOptionalNumber = (value: unknown, field: string) => {
if (value == null) return undefined;
const parsed = Number(value);
if (Number.isNaN(parsed)) throw new Error(`Invalid value for ${field}`);
return parsed;
};

View File

@ -9,7 +9,9 @@ import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/
import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service";
@Module({ @Module({
controllers: [ExpenseController], controllers: [
ExpenseController
],
providers: [ providers: [
ExpenseCreateService, ExpenseCreateService,
ExpenseUpdateService, ExpenseUpdateService,

View File

@ -18,10 +18,10 @@ export class ExpenseCreateService {
private readonly payPeriodEventService: PayPeriodEventService, private readonly payPeriodEventService: PayPeriodEventService,
) { } ) { }
//_________________________________________________________________ async createExpense(
// CREATE dto: ExpenseDto,
//_________________________________________________________________ email: string
async createExpense(dto: ExpenseDto, email: string): Promise<Result<ExpenseDto, string>> { ): Promise<Result<ExpenseDto, string>> {
try { try {
//fetch employee_id using req.user.email //fetch employee_id using req.user.email
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
@ -75,6 +75,7 @@ export class ExpenseCreateService {
return { success: true, data: created }; return { success: true, data: created };
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: 'INVALID_EXPENSE' }; return { success: false, error: 'INVALID_EXPENSE' };
} }
} }

View File

@ -10,12 +10,12 @@ export class ExpenseDeleteService {
private readonly prisma: PrismaPostgresService, private readonly prisma: PrismaPostgresService,
private readonly payPeriodEventService: PayPeriodEventService, private readonly payPeriodEventService: PayPeriodEventService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
){} ) { }
//_________________________________________________________________ async deleteExpense(
// DELETE expense_id: number,
//_________________________________________________________________ email: string
async deleteExpense(expense_id: number, email: string): Promise<Result<number, string>> { ): Promise<Result<number, string>> {
// get employee id of employee who made delete request // get employee id of employee who made delete request
const employee = await this.emailResolver.findIdByEmail(email); const employee = await this.emailResolver.findIdByEmail(email);
@ -23,7 +23,7 @@ export class ExpenseDeleteService {
// confirm ownership of expense to employee who made request // confirm ownership of expense to employee who made request
const expense = await this.prisma.expenses.findUnique({ const expense = await this.prisma.expenses.findUnique({
where: { id: expense_id}, where: { id: expense_id },
select: { select: {
timesheet: { timesheet: {
select: { select: {
@ -33,7 +33,7 @@ export class ExpenseDeleteService {
} }
}); });
if (!expense || expense.timesheet.employee_id !== employee.data) return { success: false, error: 'EXPENSE_NOT_FOUND'}; if (!expense || expense.timesheet.employee_id !== employee.data) return { success: false, error: 'EXPENSE_NOT_FOUND' };
try { try {
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
@ -56,6 +56,7 @@ export class ExpenseDeleteService {
return { success: true, data: expense_id }; return { success: true, data: expense_id };
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: `EXPENSE_NOT_FOUND` }; return { success: false, error: `EXPENSE_NOT_FOUND` };
} }
} }

View File

@ -17,10 +17,12 @@ export class ExpenseUpdateService {
private readonly typeResolver: BankCodesResolver, private readonly typeResolver: BankCodesResolver,
private readonly payPeriodEventService: PayPeriodEventService, private readonly payPeriodEventService: PayPeriodEventService,
) { } ) { }
//_________________________________________________________________
// UPDATE async updateExpense(
//_________________________________________________________________ dto: ExpenseDto,
async updateExpense(dto: ExpenseDto, email: string, employee_email?: string): Promise<Result<ExpenseDto, string>> { email: string,
employee_email?: string
): Promise<Result<ExpenseDto, string>> {
try { try {
const account_email = employee_email ?? email; const account_email = employee_email ?? email;
//fetch employee_id using req.user.email //fetch employee_id using req.user.email
@ -79,6 +81,7 @@ export class ExpenseUpdateService {
return { success: true, data: updated }; return { success: true, data: updated };
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: 'EXPENSE_NOT_FOUND' }; return { success: false, error: 'EXPENSE_NOT_FOUND' };
} }
} }

View File

@ -44,7 +44,7 @@ export class CsvExportController {
}, },
} }
); );
const csv_buffer = await this.generator.generateCsv(rows); const csv_buffer = this.generator.generateCsv(rows);
response.set({ response.set({
'Content-Type': 'text/csv; charset=utf-8', 'Content-Type': 'text/csv; charset=utf-8',

View File

@ -14,7 +14,9 @@ import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
HolidayService, HolidayService,
EmailToIdResolver, EmailToIdResolver,
], ],
controllers: [CsvExportController], controllers: [
CsvExportController
],
}) })
export class CsvExportModule { } export class CsvExportModule { }

View File

@ -11,7 +11,7 @@ export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): Inte
const map = new Map<string, InternalCsvRow>(); const map = new Map<string, InternalCsvRow>();
for (const row of rows) { for (const row of rows) {
if (row.code = VACATION) { if (row.code === VACATION) {
map.set(`${row.code}|${row.shift_date}`, row); map.set(`${row.code}|${row.shift_date}`, row);
} else { } else {
const key = `${row.code}|${row.semaine_no}`; const key = `${row.code}|${row.semaine_no}`;

View File

@ -4,7 +4,9 @@ import { CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
@Injectable() @Injectable()
export class CsvGeneratorService { export class CsvGeneratorService {
//csv builder and "mise en page" //csv builder and "mise en page"
generateCsv(rows: CsvRow[]): Buffer { generateCsv(
rows: CsvRow[]
): Buffer {
const body = rows.map(row => { const body = rows.map(row => {
const quantity_hours = (typeof row.quantite_hre === 'number') ? row.quantite_hre.toFixed(2) : ''; const quantity_hours = (typeof row.quantite_hre === 'number') ? row.quantite_hre.toFixed(2) : '';
const amount = (typeof row.montant === 'number') ? row.montant.toFixed(2) : ''; const amount = (typeof row.montant === 'number') ? row.montant.toFixed(2) : '';

View File

@ -33,7 +33,11 @@ export class CsvExportService {
* @returns The desired filtered data in semi-colon-separated format, grouped and sorted by * @returns The desired filtered data in semi-colon-separated format, grouped and sorted by
* employee and by bank codes. * employee and by bank codes.
*/ */
async collectTransaction(year: number, period_no: number, filters: Filters): Promise<CsvRow[]> { async collectTransaction(
year: number,
period_no: number,
filters: Filters
): Promise<CsvRow[]> {
const BILLABLE_SHIFT_TYPES: BillableShiftType[] = []; const BILLABLE_SHIFT_TYPES: BillableShiftType[] = [];
if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK'); if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK');
@ -70,10 +74,10 @@ export class CsvExportService {
}); });
const rows: InternalCsvRow[] = exportedShifts.map(shift => { const rows: InternalCsvRow[] = exportedShifts.map(shift => {
const employee = shift!.timesheet.employee; const employee = shift.timesheet.employee;
const week = computeWeekNumber(start, shift!.date); const week = computeWeekNumber(start, shift.date);
const type_transaction = shift!.bank_code.bank_code.charAt(0); const type_transaction = shift.bank_code.bank_code.charAt(0);
const code = Number(shift!.bank_code.bank_code.slice(1,)); const code = Number(shift.bank_code.bank_code.slice(1,));
const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code) const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code)
return { return {
@ -84,7 +88,7 @@ export class CsvExportService {
releve: 0, releve: 0,
type_transaction: type_transaction, type_transaction: type_transaction,
code: code, code: code,
quantite_hre: computeHours(shift!.start_time, shift!.end_time), quantite_hre: computeHours(shift.start_time, shift.end_time),
taux_horaire: '', taux_horaire: '',
montant: undefined, montant: undefined,
semaine_no: week, semaine_no: week,
@ -93,8 +97,8 @@ export class CsvExportService {
departem_no: undefined, departem_no: undefined,
sous_departem_no: undefined, sous_departem_no: undefined,
date_transaction: formatDate(end), date_transaction: formatDate(end),
premier_jour_absence: isPTO ? formatDate(shift!.date) : '', premier_jour_absence: isPTO ? formatDate(shift.date) : '',
dernier_jour_absence: isPTO ? formatDate(shift!.date) : '', dernier_jour_absence: isPTO ? formatDate(shift.date) : '',
} }
}); });
@ -110,8 +114,8 @@ export class CsvExportService {
exportedExpenses.map(expense => { exportedExpenses.map(expense => {
const employee = expense.timesheet.employee; const employee = expense.timesheet.employee;
const type_transaction = expense!.bank_code.bank_code.charAt(0); const type_transaction = expense.bank_code.bank_code.charAt(0);
const code = Number(expense!.bank_code.bank_code.slice(1,)) const code = Number(expense.bank_code.bank_code.slice(1,))
const week = computeWeekNumber(start, expense.date); const week = computeWeekNumber(start, expense.date);
rows.push({ rows.push({
@ -147,7 +151,7 @@ export class CsvExportService {
}); });
const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]); const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]);
const consolidated_rows = await consolidateRowHoursAndAmountByType(holiday_rows); const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows);
//requalifies regular hours into overtime when needed //requalifies regular hours into overtime when needed
const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service); const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service);

View File

@ -14,7 +14,6 @@ export type NormalizedExpense = {
amount?: number | Prisma.Decimal | null; amount?: number | Prisma.Decimal | null;
mileage?: number | Prisma.Decimal | null; mileage?: number | Prisma.Decimal | null;
attachment?: number; attachment?: number;
// bank_code_id: number;
}; };
export type NormalizedLeaveRequest = { export type NormalizedLeaveRequest = {