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
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
// import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
@ -10,7 +10,7 @@ export default tseslint.config(
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
// eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common';
import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto';
import { HttpService } from '@nestjs/axios';
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';
@Injectable()
@ -14,41 +13,11 @@ export class ChatbotService {
const { data } = await firstValueFrom(this.httpService.post(
'https://n8nai.targo.ca/webhook/chatty-Mcbot',
{ userInput: body.userInput, userId: email, sessionId: this.sessionId, pageContext: body.pageContext ?? undefined }
));
)) as ChatbotResponseDto;
return {
text: data[0].output,
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';
export class Message {
@IsString()
text!: string;
@IsBoolean()
sent!: boolean;
@IsString() text: string;
@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 {
@IsString()
name: string;
@IsString()
description: string;
@IsArray()
features: string[];
@IsString()
path?: string;
@IsString() name: string;
@IsString() description: string;
@IsArray() features: string[];
@IsString() @IsOptional() 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 { PageContextDto } from './page-context.dto';
export class UserMessageDto {
@IsString()
@IsNotEmpty()
@Transform(({ value }) => value.trim())
userInput!: string;
@IsOptional() @Type(() => PageContextDto) pageContext?: PageContextDto | undefined;
@IsString()
@IsNotEmpty()
@IsString() userInput: string;
@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 { UserDto } from "src/identity-and-account/users-management/user.dto";
export const Access = createParamDecorator(
(data:string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
export interface AuthenticatedRequest extends Request {
user: UserDto;
}
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;
},
);

View File

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

View File

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

View File

@ -1,7 +1,7 @@
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 { Result } from "src/common/errors/result-error.factory";
type Tx = Prisma.TransactionClient | PrismaClient;
@ -9,7 +9,10 @@ type Tx = Prisma.TransactionClient | PrismaClient;
export class FullNameResolver {
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 employee = await db.employees.findUnique({
where: { id: employee_id },

View File

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

View File

@ -1,8 +1,8 @@
import { Injectable } from "@nestjs/common";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { EmailToIdResolver } from "./email-id.mapper";
import { Result } from "src/common/errors/result-error.factory";
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 { Prisma } from "prisma/postgres/generated/prisma/client/postgres/client";
@ -16,7 +16,11 @@ export class EmployeeTimesheetResolver {
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 employee_id = await this.emailResolver.findIdByEmail(email);
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>;
//standard update Aproval
async updateApproval(id: number, is_approved: boolean): Promise<T> {
async updateApproval(
id: number,
is_approved: boolean
): Promise<T> {
try {
return await this.delegate.update({
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 {
return await this.delegateFor(tx).update({
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({
imports: [ PassportModule.register({
session: true,
defaultStrategy: 'openidconnect'
}), UsersModule, ],
imports: [
PassportModule.register({
session: true,
defaultStrategy: 'openidconnect'
}), UsersModule,
],
providers: [
AuthentikAuthService,
AuthentikStrategy,
ExpressSessionSerializer,
UsersService,
],
exports: [ AuthentikAuthService ],
controllers: [AuthController],
exports: [
AuthentikAuthService
],
controllers: [
AuthController
],
})
export class AuthenticationModule {}
export class AuthenticationModule { }

View File

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

View File

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

View File

@ -9,7 +9,7 @@ export class ExpressSessionSerializer extends PassportSerializer {
}
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){
done(new UnauthorizedException('Deserialize user error'), payload);
}

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/identity-and-account/users-management/services/users.service';
import { UserDto } from 'src/identity-and-account/users-management/user.dto';
@Injectable()
export class AuthentikAuthService {
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);
return user;

View File

@ -39,9 +39,9 @@ export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidcon
async validate(
_issuer: string,
profile: Profile,
_profile: Profile,
_context: any,
_idToken: string,
idToken: string,
_accessToken: string,
_refreshToken: string,
_params: any,
@ -50,9 +50,9 @@ export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidcon
try {
const components = _idToken.split('.');
const components = idToken.split('.');
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);

View File

@ -18,13 +18,17 @@ export class EmployeesController {
@Get('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);
}
@Get('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>> {
return await this.getService.findOneDetailedProfile(email, employee_email);
}
@ -37,13 +41,17 @@ export class EmployeesController {
@Post('create')
@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);
}
@Patch('update')
@ModuleAccessAllowed(ModulesEnum.employee_management)
async updateEmployee(@Body() dto:EmployeeDetailedUpsertDto){
async updateEmployee(
@Body() dto:EmployeeDetailedUpsertDto
){
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';
@Module({
imports: [],
controllers: [EmployeesController],
controllers: [
EmployeesController
],
providers: [
EmployeesGetService,
EmployeesUpdateService,
@ -16,6 +17,8 @@ import { EmployeesCreateService } from 'src/identity-and-account/employees/servi
AccessGetService,
EmailToIdResolver
],
exports: [EmployeesGetService],
exports: [
EmployeesGetService
],
})
export class EmployeesModule { }

View File

@ -10,7 +10,9 @@ export class HomePageController {
@Get('help')
@ModuleAccessAllowed(ModulesEnum.dashboard)
async getIntroductionHelper(@Access('email') email: string) {
async getIntroductionHelper(
@Access('email') email: string
) {
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";
@Module({
controllers: [HomePageController],
providers: [HomePageService, EmailToIdResolver],
exports: [HomePageService],
controllers: [
HomePageController
],
providers: [
HomePageService,
EmailToIdResolver
],
exports: [
HomePageService
],
})
export class HomePageModule { };

View File

@ -10,7 +10,9 @@ export class HomePageService {
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);
if (!user_id.success) return { success: false, error: 'INVALID_EMAIL' };

View File

@ -12,14 +12,19 @@ export class PreferencesController {
@Patch('update')
@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>> {
return this.service.updatePreferences(email, payload);
}
@Get()
@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);
}

View File

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

View File

@ -11,7 +11,10 @@ export class PreferencesService {
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 user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
@ -42,7 +45,10 @@ export class PreferencesService {
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);
if (!user_id.success) return { success: false, error: user_id.error }

View File

@ -1,10 +1,10 @@
import { IsBoolean } from "class-validator";
export class ModuleAccess {
@IsBoolean() timesheets!: boolean;
@IsBoolean() timesheets_approval!: boolean;
@IsBoolean() employee_list!: boolean;
@IsBoolean() employee_management!: boolean;
@IsBoolean() personal_profile!: boolean;
@IsBoolean() dashboard!: boolean;
@IsBoolean() timesheets: boolean;
@IsBoolean() timesheets_approval: boolean;
@IsBoolean() employee_list: boolean;
@IsBoolean() employee_management: boolean;
@IsBoolean() personal_profile: 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 { Result } from "src/common/errors/result-error.factory";
import { ModuleAccess } from "src/identity-and-account/user-module-access/module-acces.dto";
@ -16,7 +16,9 @@ export class ModuleAccessController {
@Get()
@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>> {
await this.getService.findModuleAccess(email, employee_email);
return { success: true, data: true };
@ -24,7 +26,10 @@ export class ModuleAccessController {
@Patch('update')
@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>> {
await this.updateService.updateModuleAccess(email, dto, employee_email);
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";
@Module({
controllers: [ModuleAccessController],
providers: [AccessUpdateService, AccessGetService, EmailToIdResolver],
exports: [AccessGetService],
controllers: [
ModuleAccessController
],
providers: [
AccessUpdateService,
AccessGetService,
EmailToIdResolver
],
exports: [
AccessGetService
],
})
export class ModuleAccessModule { }

View File

@ -12,7 +12,10 @@ export class AccessGetService {
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 user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };

View File

@ -11,7 +11,11 @@ export class AccessUpdateService {
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 user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
@ -52,7 +56,10 @@ export class AccessUpdateService {
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 user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
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 {
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({
where: { email },
include: {

View File

@ -6,5 +6,5 @@ export class UserDto {
@IsString() last_name: string;
@IsEmail() email: 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';
@Module({
imports: [PrismaPostgresModule],
providers: [UsersService],
exports: [UsersService],
imports: [
PrismaPostgresModule
],
providers: [
UsersService
],
exports: [
UsersService
],
})
export class UsersModule {}

View File

@ -1,8 +1,8 @@
import 'reflect-metadata';
import * as nodeCrypto from 'crypto';
if (!(globalThis as any).crypto) {
(globalThis as any).crypto = nodeCrypto;
}
// import * as nodeCrypto from 'crypto';
// if (!(globalThis as any).crypto) {
// (globalThis as any).crypto = nodeCrypto;
// }
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
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
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 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)
export class BankCodesControllers {
constructor(private readonly bankCodesService: BankCodesService) { }
//_____________________________________________________________________________________________
// Deprecated or unused methods
//_____________________________________________________________________________________________
@Post()
create(@Body() dto: Prisma.BankCodesCreateInput
create(
@Body() dto: Prisma.BankCodesCreateInput
): Promise<Result<boolean, string>> {
return this.bankCodesService.create(dto);
}
@ -24,7 +22,9 @@ export class BankCodesControllers {
}
@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>> {
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";
@Module({
controllers: [BankCodesControllers],
providers: [BankCodesService, PrismaPostgresService],
controllers: [
BankCodesControllers
],
providers: [
BankCodesService,
PrismaPostgresService
],
})
export class BankCodesModule {}

View File

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

View File

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

View File

@ -8,7 +8,11 @@ export class BankedHoursService {
constructor(private readonly prisma: PrismaPostgresService) { }
//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' };
try {
@ -50,8 +54,6 @@ export class BankedHoursService {
} else if (type === 'WITHDRAW_BANKED') {
if (asked_hours > banked_hours) {
return { success: true, data: banked_hours } as Result<number, string>;
} else {
}
await tx.paidTimeOff.update({
where: { employee_id: employee.id },
@ -69,7 +71,8 @@ export class BankedHoursService {
return result;
} 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`
*/
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 {
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720'];
const holiday_week_start = getWeekStart(holiday_date);
@ -42,7 +46,7 @@ export class HolidayService {
});
if (!employee)
return {success: false, error: 'EMPLOYEE_NOT_FOUND'};
return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
const shifts = await this.prisma.shifts.findMany({
where: {
@ -76,11 +80,16 @@ export class HolidayService {
const average_daily_hours = capped_total / 20;
return { success: true, data: average_daily_hours };
} catch (error) {
console.error(error);
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);
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) { }
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' };
//fetch modifier

View File

@ -27,11 +27,21 @@ type WeekOvertimeSummary = {
@Injectable()
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) { }
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 week_start = getWeekStart(date);

View File

@ -8,7 +8,9 @@ import { Result } from "src/common/errors/result-error.factory";
export class SickLeaveService {
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 FOURTEEN_DAYS = 1000 * 60 * 60 * 24 * 14;
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()) {
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());
@ -69,7 +71,10 @@ export class SickLeaveService {
}
// 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 {
const new_pto_entry = await this.prisma.paidTimeOff.create({
data: {
@ -88,12 +93,16 @@ export class SickLeaveService {
return { success: true, data: new_pto_entry };
} catch (error) {
return { success: false, error };
return { success: false, error: '' + error };
}
}
// 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 {
const update_pto = await this.prisma.paidTimeOff.update({
where: {
@ -107,11 +116,15 @@ export class SickLeaveService {
return { success: true, data: update_pto };
} 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' };
try {
@ -154,74 +167,9 @@ export class SickLeaveService {
return result;
} catch (error) {
console.error(error);
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,
) { }
//switch employeeId for email
async calculateVacationPay(email: string, start_date: Date, days_requested: number, modifier: number): Promise<Result<number, string>> {
const employee_id = await this.emailResolver.findIdByEmail(email);
if (!employee_id.success) return { success: false, error: employee_id.error }
@ -20,7 +19,7 @@ export class VacationService {
where: { id: employee_id.data },
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;
@ -110,10 +109,8 @@ export class VacationService {
});
return result;
} catch (error) {
console.error(error);
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 { Result } from "src/common/errors/result-error.factory";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
@ -18,7 +18,10 @@ export class ExpenseController {
@Post('create')
@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');
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 { 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
export const normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Result<NormalizedExpense, string>> => {
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";
@Module({
controllers: [ExpenseController],
controllers: [
ExpenseController
],
providers: [
ExpenseCreateService,
ExpenseUpdateService,

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): Inte
const map = new Map<string, InternalCsvRow>();
for (const row of rows) {
if (row.code = VACATION) {
if (row.code === VACATION) {
map.set(`${row.code}|${row.shift_date}`, row);
} else {
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()
export class CsvGeneratorService {
//csv builder and "mise en page"
generateCsv(rows: CsvRow[]): Buffer {
generateCsv(
rows: CsvRow[]
): Buffer {
const body = rows.map(row => {
const quantity_hours = (typeof row.quantite_hre === 'number') ? row.quantite_hre.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
* 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[] = [];
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 employee = shift!.timesheet.employee;
const week = computeWeekNumber(start, shift!.date);
const type_transaction = shift!.bank_code.bank_code.charAt(0);
const code = Number(shift!.bank_code.bank_code.slice(1,));
const employee = shift.timesheet.employee;
const week = computeWeekNumber(start, shift.date);
const type_transaction = shift.bank_code.bank_code.charAt(0);
const code = Number(shift.bank_code.bank_code.slice(1,));
const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code)
return {
@ -84,7 +88,7 @@ export class CsvExportService {
releve: 0,
type_transaction: type_transaction,
code: code,
quantite_hre: computeHours(shift!.start_time, shift!.end_time),
quantite_hre: computeHours(shift.start_time, shift.end_time),
taux_horaire: '',
montant: undefined,
semaine_no: week,
@ -93,8 +97,8 @@ export class CsvExportService {
departem_no: undefined,
sous_departem_no: undefined,
date_transaction: formatDate(end),
premier_jour_absence: isPTO ? formatDate(shift!.date) : '',
dernier_jour_absence: isPTO ? formatDate(shift!.date) : '',
premier_jour_absence: isPTO ? formatDate(shift.date) : '',
dernier_jour_absence: isPTO ? formatDate(shift.date) : '',
}
});
@ -110,8 +114,8 @@ export class CsvExportService {
exportedExpenses.map(expense => {
const employee = expense.timesheet.employee;
const type_transaction = expense!.bank_code.bank_code.charAt(0);
const code = Number(expense!.bank_code.bank_code.slice(1,))
const type_transaction = expense.bank_code.bank_code.charAt(0);
const code = Number(expense.bank_code.bank_code.slice(1,))
const week = computeWeekNumber(start, expense.date);
rows.push({
@ -147,7 +151,7 @@ export class CsvExportService {
});
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
const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service);

View File

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