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

This commit is contained in:
Nicolas Drolet 2025-08-05 08:47:09 -04:00
commit bd4fc5fd30
64 changed files with 3062 additions and 3062 deletions

File diff suppressed because it is too large Load Diff

44
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.11.1",
"class-transformer": "^0.5.1",
@ -849,9 +850,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true,
"dependencies": {
"@eslint/core": "^0.15.1",
@ -2460,6 +2461,18 @@
"@nestjs/core": "^11.0.0"
}
},
"node_modules/@nestjs/schedule": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz",
"integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==",
"dependencies": {
"cron": "4.3.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/schematics": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz",
@ -3349,6 +3362,11 @@
"@types/node": "*"
}
},
"node_modules/@types/luxon": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -5577,6 +5595,18 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cron": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz",
"integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==",
"dependencies": {
"@types/luxon": "~3.6.0",
"luxon": "~3.6.0"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -8401,6 +8431,14 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",

View File

@ -26,6 +26,7 @@
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.11.1",
"class-transformer": "^0.5.1",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "shifts_archive" ALTER COLUMN "archive_at" SET DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,54 @@
/*
Warnings:
- You are about to drop the column `expense_code_id` on the `expenses` table. All the data in the column will be lost.
- You are about to drop the column `shift_code_id` on the `shifts` table. All the data in the column will be lost.
- You are about to drop the `expense_codes` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `shift_codes` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `bank_code_id` to the `expenses` table without a default value. This is not possible if the table is not empty.
- Added the required column `bank_code_id` to the `leave_requests` table without a default value. This is not possible if the table is not empty.
- Added the required column `bank_code_id` to the `shifts` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "expenses" DROP CONSTRAINT "expenses_expense_code_id_fkey";
-- DropForeignKey
ALTER TABLE "shifts" DROP CONSTRAINT "shifts_shift_code_id_fkey";
-- AlterTable
ALTER TABLE "expenses" DROP COLUMN "expense_code_id",
ADD COLUMN "bank_code_id" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "leave_requests" ADD COLUMN "bank_code_id" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "shifts" DROP COLUMN "shift_code_id",
ADD COLUMN "bank_code_id" INTEGER NOT NULL;
-- DropTable
DROP TABLE "expense_codes";
-- DropTable
DROP TABLE "shift_codes";
-- CreateTable
CREATE TABLE "bank_codes" (
"id" SERIAL NOT NULL,
"type" TEXT NOT NULL,
"categorie" TEXT NOT NULL,
"modifier" DOUBLE PRECISION NOT NULL,
"bank_code" TEXT NOT NULL,
CONSTRAINT "bank_codes_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "leave_requests" ADD CONSTRAINT "leave_requests_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "shifts" ADD CONSTRAINT "shifts_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,16 @@
/*
Warnings:
- You are about to drop the column `expense_code_id` on the `expenses_archive` table. All the data in the column will be lost.
- You are about to drop the column `shift_code_id` on the `shifts_archive` table. All the data in the column will be lost.
- Added the required column `bank_code_id` to the `expenses_archive` table without a default value. This is not possible if the table is not empty.
- Added the required column `bank_code_id` to the `shifts_archive` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "expenses_archive" DROP COLUMN "expense_code_id",
ADD COLUMN "bank_code_id" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "shifts_archive" DROP COLUMN "shift_code_id",
ADD COLUMN "bank_code_id" INTEGER NOT NULL;

View File

@ -5,7 +5,7 @@
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["views"]
}
@ -26,8 +26,8 @@ model Users {
employee Employees? @relation("UserEmployee")
customer Customers? @relation("UserCustomer")
oauth_access_tokens OAuthAccessTokens[] @relation("UserOAuthAccessToken")
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
@@map("users")
}
@ -41,9 +41,9 @@ model Employees {
first_work_day DateTime @db.Date
last_work_day DateTime? @db.Date
supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id])
supervisor_id Int?
managed_employees Employees[] @relation("EmployeeSupervisor")
supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id])
supervisor_id Int?
crew Employees[] @relation("EmployeeSupervisor")
archive EmployeesArchive[] @relation("EmployeeToArchive")
timesheet Timesheets[] @relation("TimesheetEmployee")
@ -54,20 +54,20 @@ model Employees {
}
model EmployeesArchive {
id Int @id @default(autoincrement())
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
employee_id Int
archived_at DateTime @default(now())
id Int @id @default(autoincrement())
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
employee_id Int
archived_at DateTime @default(now())
user_id String @db.Uuid
user Users @relation("UsersToEmployeesToArchive",fields: [user_id], references: [id])
first_name String
last_name String
user_id String @db.Uuid
user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id])
first_name String
last_name String
external_payroll_id Int
company_code Int
first_Work_Day DateTime @db.Date
last_work_day DateTime @db.Date
first_Work_Day DateTime @db.Date
last_work_day DateTime @db.Date
supervisor_id Int?
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])
@ -93,7 +93,7 @@ model CustomersArchive {
user_id String @db.Uuid
user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id])
invoice_id Int? @unique
invoice_id Int? @unique
@@map("customers_archive")
}
@ -102,6 +102,8 @@ model LeaveRequests {
id Int @id @default(autoincrement())
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
employee_id Int
bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
bank_code_id Int
leave_type LeaveTypes
start_date_time DateTime @db.Date
end_date_time DateTime? @db.Date
@ -130,7 +132,7 @@ model LeaveRequestsArchive {
//pay-period vue
view PayPeriods {
period_number Int @id
period_number Int @id //do not try to fix it, Prisma is working on a fix for views
start_date DateTime @db.Date
end_date DateTime @db.Date
year Int
@ -164,15 +166,15 @@ model TimesheetsArchive {
}
model Shifts {
id Int @id @default(autoincrement())
timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id])
timesheet_id Int
shift_code ShiftCodes @relation("ShiftShiftCode", fields: [shift_code_id], references: [id])
shift_code_id Int
description String?
date DateTime @db.Date
start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0)
id Int @id @default(autoincrement())
timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id])
timesheet_id Int
bank_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id])
bank_code_id Int
description String?
date DateTime @db.Date
start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0)
archive ShiftsArchive[] @relation("ShiftsToArchive")
@ -180,41 +182,45 @@ model Shifts {
}
model ShiftsArchive {
id Int @id @default(autoincrement())
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
shift_id Int
archive_at DateTime
timesheet_id Int
shift_code_id Int
description String?
date DateTime @db.Date
start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0)
id Int @id @default(autoincrement())
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
shift_id Int
archive_at DateTime @default(now())
timesheet_id Int
bank_code_id Int
description String?
date DateTime @db.Date
start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0)
@@map("shifts_archive")
}
model ShiftCodes {
id Int @id @default(autoincrement())
shift_type String
bank_code String
model BankCodes {
id Int @id @default(autoincrement())
type String
categorie String
modifier Float
bank_code String
shift Shifts[] @relation("ShiftShiftCode")
shifts Shifts[] @relation("ShiftBankCodes")
expenses Expenses[] @relation("ExpenseBankCodes")
leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes")
@@map("shift_codes")
@@map("bank_codes")
}
model Expenses {
id Int @id @default(autoincrement())
timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id])
id Int @id @default(autoincrement())
timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id])
timesheet_id Int
expense_code ExpenseCodes @relation("ExpenseExpenseCode", fields: [expense_code_id], references: [id])
expense_code_id Int
date DateTime @db.Date
amount Decimal @db.Money
bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
bank_code_id Int
date DateTime @db.Date
amount Decimal @db.Money
attachement String?
description String?
is_approved Boolean @default(false)
is_approved Boolean @default(false)
supervisor_comment String?
archive ExpensesArchive[] @relation("ExpensesToArchive")
@ -228,7 +234,7 @@ model ExpensesArchive {
expense_id Int
timesheet_id Int
archived_at DateTime @default(now())
expense_code_id Int
bank_code_id Int
date DateTime @db.Date
amount Decimal @db.Money
attachement String?
@ -239,16 +245,6 @@ model ExpensesArchive {
@@map("expenses_archive")
}
model ExpenseCodes {
id Int @id @default(autoincrement())
expense_type String
bank_code String
expense Expenses[] @relation("ExpenseExpenseCode")
@@map("expense_codes")
}
model OAuthAccessTokens {
id String @id @default(cuid())
user Users @relation("UserOAuthAccessToken", fields: [user_id], references: [id])

View File

@ -9,32 +9,37 @@ import { OauthAccessTokensModule } from './modules/oauth-access-tokens/oauth-acc
import { CustomersModule } from './modules/customers/customers.module';
import { EmployeesModule } from './modules/employees/employees.module';
import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module';
import { ShiftCodesModule } from './modules/shift-codes/shift-codes.module';
import { ShiftsModule } from './modules/shifts/shifts.module';
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
import { AuthenticationModule } from './modules/authentication/auth.module';
import { ExpensesModule } from './modules/expenses/expenses.module';
import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ArchivalModule } from './modules/archival/archival.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
@Module({
imports: [
PrismaModule,
HealthModule,
UsersModule,
OauthAccessTokensModule,
ScheduleModule.forRoot(),
ArchivalModule,
AuthenticationModule,
BankCodesModule,
BusinessLogicsModule,
CustomersModule,
EmployeesModule,
LeaveRequestsModule,
ExpensesModule,
ExpenseCodesModule,
ShiftCodesModule,
HealthModule,
LeaveRequestsModule,
OauthAccessTokensModule,
PayperiodsModule,
PrismaModule,
ShiftsModule,
TimesheetsModule,
AuthenticationModule,
PayperiodsModule,
UsersModule,
],
controllers: [AppController, HealthController],
providers: [AppService],
providers: [AppService, OvertimeService],
})
export class AppModule {}

View File

@ -1,4 +1,9 @@
import 'reflect-metadata';
//import and if case for @nestjs/schedule Cron jobs
import * as nodeCrypto from 'crypto';
if(!(globalThis as any).crypto) {
(globalThis as any).crypto = nodeCrypto;
}
import { ModuleRef, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

View File

@ -0,0 +1,34 @@
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

@ -0,0 +1,34 @@
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 { EmployeesArchive, Roles as RoleEnum } from '@prisma/client';
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { EmployeesService } from "src/modules/employees/services/employees.service";
@ApiTags('Employee Archives')
@UseGuards(JwtAuthGuard)
@Controller('archives/employees')
export class EmployeesArchiveController {
constructor(private readonly employeesService: EmployeesService) {}
@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.employeesService.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.employeesService.findOneArchived(id);
}catch {
throw new NotFoundException(`Archived employee #${id} not found`);
}
}
}

View File

@ -0,0 +1,33 @@
import { UseGuards, Controller, Get, Param, ParseIntPipe, NotFoundException } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ExpensesService } from "src/modules/expenses/services/expenses.service";
@ApiTags('Expense Archives')
@UseGuards(JwtAuthGuard)
@Controller('archives/expenses')
export class ExpensesArchiveController {
constructor(private readonly expensesService: ExpensesService) {}
@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

@ -0,0 +1,33 @@
import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
@ApiTags('LeaveRequests Archives')
@UseGuards(JwtAuthGuard)
@Controller('archives/leaveRequests')
export class LeaveRequestsArchiveController {
constructor(private readonly leaveRequestsService: LeaveRequestsService) {}
@Get()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'List of archived leaveRequests'})
@ApiResponse({ status: 200, description: 'List of archived leaveRequests', isArray: true })
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
return this.leaveRequestsService.findAllArchived();
}
@Get()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'})
@ApiResponse({ status: 200, description: 'Archived leaveRequest found'})
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestsArchive> {
try{
return await this.leaveRequestsService.findOneArchived(id);
}catch {
throw new NotFoundException(`Archived leaveRequest #${id} not found`);
}
}
}

View File

@ -0,0 +1,33 @@
import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ShiftsService } from "src/modules/shifts/services/shifts.service";
@ApiTags('Shift Archives')
@UseGuards(JwtAuthGuard)
@Controller('archives/shifts')
export class ShiftsArchiveController {
constructor(private readonly shiftsService:ShiftsService) {}
@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

@ -0,0 +1,34 @@
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 { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client';
import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
@ApiTags('Timesheet Archives')
@UseGuards(JwtAuthGuard)
@Controller('archives/timesheets')
export class TimesheetsArchiveController {
constructor(private readonly timesheetsService: TimesheetsService) {}
@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

@ -0,0 +1,40 @@
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { ExpensesService } from "src/modules/expenses/services/expenses.service";
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
import { ShiftsService } from "src/modules/shifts/services/shifts.service";
import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service";
@Injectable()
export class ArchivalService {
private readonly logger = new Logger(ArchivalService.name);
constructor(
private readonly timesheetsService: TimesheetsService,
private readonly expensesService: ExpensesService,
private readonly shiftsService: ShiftsService,
private readonly leaveRequestsService: LeaveRequestsService,
) {}
@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

@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { BankCodesControllers } from "./controllers/bank-codes.controller";
import { BankCodesService } from "./services/bank-codes.services";
@Module({
controllers: [BankCodesControllers],
providers: [BankCodesService, PrismaService],
})
export class BankCodesModule {}

View File

@ -0,0 +1,46 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common";
import { BankCodesService } from "../services/bank-codes.services";
import { CreateBankCodeDto } from "../dtos/create-bank-codes";
import { UpdateBankCodeDto } from "../dtos/update-bank-codes";
import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger";
@Controller('bank-codes')
export class BankCodesControllers {
constructor(private readonly bankCodesService: BankCodesService) {}
@Post()
@ApiOperation({ summary: 'Create a new bank code' })
@ApiResponse({ status: 201, description: 'Bank code successfully created.' })
@ApiBadRequestResponse({ description: 'Invalid input data.' })
create(@Body() dto: CreateBankCodeDto) {
return this.bankCodesService.create(dto);
}
@Get()
@ApiOperation({ summary: 'Retrieve all bank codes' })
@ApiResponse({ status: 200, description: 'List of bank codes.' })
findAll() {
return this.bankCodesService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Retrieve a bank code by its ID' })
@ApiNotFoundResponse({ description: 'Bank code not found.' })
findOne(@Param('id', ParseIntPipe) id: number){
return this.bankCodesService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update an existing bank code' })
@ApiNotFoundResponse({ description: 'Bank code not found.' })
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) {
return this.bankCodesService.update(id, dto)
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a bank code' })
@ApiNotFoundResponse({ description: 'Bank code not found.' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.bankCodesService.remove(id);
}
}

View File

@ -0,0 +1,20 @@
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
export class CreateBankCodeDto {
@IsString()
@IsNotEmpty()
type: string;
@IsString()
@IsNotEmpty()
categorie: string;
@IsNumber()
@IsNotEmpty()
modifier: number;
@IsString()
@IsNotEmpty()
bank_code: string;
}

View File

@ -0,0 +1,34 @@
import { ApiProperty } from "@nestjs/swagger";
export class BankCodesEntity {
@ApiProperty({
example: 1,
description: 'Unique ID of a bank-code (auto-generated)',
})
id: number;
@ApiProperty({
example: 'regular, vacation, emergency, sick, parental, etc',
description: 'Type of codes',
})
type: string;
@ApiProperty({
example: 'shift, expense, leave',
description: 'categorie of the related code',
})
categorie: string;
@ApiProperty({
example: '0, 0.72, 1, 1.5, 2',
description: 'modifier number to apply to salary',
})
modifier: number;
@ApiProperty({
example: 'G1, G345, G501, G43, G700',
description: 'codes given by the bank',
})
bank_code: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateBankCodeDto } from "./create-bank-codes";
export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {}

View File

@ -0,0 +1,38 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateBankCodeDto } from "../dtos/create-bank-codes";
import { BankCodes } from "@prisma/client";
import { UpdateBankCodeDto } from "../dtos/update-bank-codes";
@Injectable()
export class BankCodesService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateBankCodeDto): Promise<BankCodes>{
return this.prisma.bankCodes.create({ data: dto })
}
findAll() {
return this.prisma.bankCodes.findMany();
}
async findOne(id: number) {
const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} });
if(!bankCode) {
throw new NotFoundException(`Bank Code #${id} not found`);
}
return bankCode;
}
async update(id:number, dto: UpdateBankCodeDto) {
await this.prisma.bankCodes.update({ where: { id }, data: dto });
}
async remove(id: number) {
await this.findOne(id);
return this.prisma.bankCodes.delete({ where: {id} });
}
}

View File

@ -0,0 +1,32 @@
import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
//import { AfterHoursService } from "./services/after-hours.service";
import { HolidayService } from "./services/holiday.service";
import { OvertimeService } from "./services/overtime.service";
import { SickLeaveService } from "./services/sick-leave.service";
import { VacationService } from "./services/vacation.service";
import { MileageService } from "./services/mileage.service";
//AfterHours is not used, need to clarify infos before implementing into shifts.service
@Module({
providers: [
PrismaService,
//AfterHoursService,
HolidayService,
MileageService,
OvertimeService,
SickLeaveService,
VacationService
],
exports: [
//AfterHoursService,
HolidayService,
MileageService,
OvertimeService,
SickLeaveService,
VacationService,
],
})
export class BusinessLogicsModule {}

View File

@ -0,0 +1,79 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
//THIS SERVICE IS NOT USED RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING
@Injectable()
export class AfterHoursService {
private readonly logger = new Logger(AfterHoursService.name);
private static readonly BUSINESS_START = 7;
private static readonly BUSINESS_END = 18;
private static readonly ROUND_MINUTES = 15;
constructor(private readonly prisma: PrismaService) {}
private getPreBusinessMinutes(start: Date, end: Date): number {
const bizStart = new Date(start);
bizStart.setHours(AfterHoursService.BUSINESS_START, 0,0,0);
if (end>= start || start >= bizStart) {
return 0;
}
const segmentEnd = end < bizStart ? end : bizStart;
const minutes = (segmentEnd.getTime() - start.getTime()) / 60000;
this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`);
return minutes;
}
private getPostBusinessMinutes(start: Date, end: Date): number {
const bizEnd = new Date(start);
bizEnd.setHours(AfterHoursService.BUSINESS_END,0,0,0);
if( end <= bizEnd ) {
return 0;
}
const segmentStart = start > bizEnd ? start : bizEnd;
const minutes = (end.getTime() - segmentStart.getTime()) / 60000;
this.logger.debug(`getPostBusinessMintutes -> ${minutes.toFixed(1)}min`);
return minutes;
}
private roundToNearestQUarterMinute(minutes: number): number {
const rounded = Math.round(minutes / AfterHoursService.ROUND_MINUTES)
* AfterHoursService.ROUND_MINUTES;
this.logger.debug(`roundToNearestQuarterMinute -> raw=${minutes.toFixed(1)}min, rounded= ${rounded}min`);
return rounded;
}
public computeAfterHours(start: Date, end:Date): number {
if(end.getTime() <= start.getTime()) {
throw new BadRequestException('The end cannot be before the starting of the shift');
}
if (start.toDateString() !== end.toDateString()) {
throw new BadRequestException('you cannot enter a shift that start in a day and end in the next' +
'You must create 2 instances, one on the first day and the second during the next day.');
}
const preMin = this.getPreBusinessMinutes(start, end);
const postMin = this.getPostBusinessMinutes(start, end);
const rawAftermin = preMin + postMin;
const roundedMin = this.roundToNearestQUarterMinute(rawAftermin);
const hours = roundedMin / 60;
const result = parseFloat(hours.toFixed(2));
this.logger.debug(`computeAfterHours -> rawAfterMin= ${rawAftermin.toFixed(1)}min, +
rounded = ${roundedMin}min, hours = ${result.toFixed(2)}`);
return result;
}
}

View File

@ -0,0 +1,56 @@
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
@Injectable()
export class HolidayService {
private readonly logger = new Logger(HolidayService.name);
constructor(private readonly prisma: PrismaService) {}
//return the sunday of the current week that includes the holiday
private getWeekStart(date: Date): Date {
const day = new Date(date);
const offset = day.getDay();
day.setDate(day.getDate() - offset);
day.setHours(0,0,0,0);
return day;
}
//rounds minutes to 5s
private computeHours(start: Date, end: Date): number {
const durationMS = end.getTime() - start.getTime();
const totalMinutes = durationMS / 60000;
const rounded = Math.round(totalMinutes / 5) * 5;
return rounded / 60;
}
private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> {
//sets the end of the window to 1ms before the week with the holiday
const holidayWeekStart = this.getWeekStart(holidayDate);
const windowEnd = new Date(holidayWeekStart.getTime() - 1);
//sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday
const windowStart = new Date(windowEnd.getTime() - 28 * 24 * 60 * 60000 + 1 )
const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700'];
//fetches all shift of the employee in said window ( 4 completed weeks )
const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId } ,
date: { gte: windowStart, lte: windowEnd },
bank_code: { bank_code: { in: validCodes } },
},
select: { date: true, start_time: true, end_time: true },
});
const totalHours = shifts.map(s => this.computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
const dailyHours = totalHours / 20;
return dailyHours;
}
async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> {
const hours = await this. computeHoursPrevious4Weeks(employeeId, holidayDate);
const dailyRate = Math.min(hours, 8);
this.logger.debug(`Holiday pay calculation: hours=${hours.toFixed(2)}`);
return dailyRate * modifier;
}
}

View File

@ -0,0 +1,36 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
import { Decimal } from "@prisma/client/runtime/library";
@Injectable()
export class MileageService {
private readonly logger = new Logger(MileageService.name);
constructor(private readonly prisma: PrismaService) {}
public async calculateReimbursement(amount: number, bankCodeId: number): Promise<number> {
if(amount < 0) {
throw new BadRequestException(`The amount most be higher than 0`);
}
//fetch modifier
const bankCode = await this.prisma.bankCodes.findUnique({
where: { id: bankCodeId },
select: { modifier: true, type: true },
});
if(!bankCode) {
throw new BadRequestException(`bank_code ${bankCodeId} not found`);
}
if(bankCode.type !== 'mileage') {
this.logger.warn(`bank_code ${bankCodeId} of type ${bankCode.type} is used for mileage`)
}
//calculate total amount to reimburs
const reimboursement = amount * bankCode.modifier;
const result = parseFloat(reimboursement.toFixed(2));
this.logger.debug(`calculateReimbursement -> amount= ${amount}, modifier= ${bankCode.modifier}, total= ${result}`);
return result;
}
}

View File

@ -0,0 +1,82 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
@Injectable()
export class OvertimeService {
private logger = new Logger(OvertimeService.name);
private dailyMax = 8; // maximum for regular hours per day
private weeklyMax = 40; //maximum for regular hours per week
constructor(private prisma: PrismaService) {}
// calculate decimal hours rounded to nearest 5 min
computedHours(start: Date, end: Date): number {
const durationMs = end.getTime() - start.getTime();
const totalMinutes = durationMs / 60000;
//rounded to 5 min
const rounded = Math.round(totalMinutes / 5) * 5;
const hours = rounded / 60;
this.logger.debug(`computedHours: raw=${totalMinutes.toFixed(1)}min rounded = ${rounded}min (${hours.toFixed(2)}h)`);
return hours;
}
//calculate Daily overtime
getDailyOvertimeHours(start: Date, end: Date): number {
const hours = this.computedHours(start, end);
const overtime = Math.max(0, hours - this.dailyMax);
this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`);
return overtime;
}
//sets first day of the week to be sunday
private getWeekStart(date:Date): Date {
const d = new Date(date);
const day = d.getDay(); // return sunday = 0, monday = 1, etc
d.setDate(d.getDate() - day);
d.setHours(0,0,0,0,); // puts start of the week at sunday morning at 00:00
return d;
}
//sets last day of the week to be saturday
private getWeekEnd(startDate:Date): Date {
const d = new Date(startDate);
d.setDate(d.getDate() +6); //sets last day to be saturday
d.setHours(23,59,59,999); //puts end of the week at saturday night at 00:00 minus 1ms
return d;
}
//calculate Weekly overtime
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
const weekStart = this.getWeekStart(refDate);
const weekEnd = this.getWeekEnd(weekStart);
//fetches all shifts containing hours
const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId, shift: {
every: {date: { gte: weekStart, lte: weekEnd } }
},
},
},
select: { start_time: true, end_time: true },
});
//calculate total hours of those shifts minus weekly Max to find total overtime hours
const total = shifts.map(shift => this.computedHours(shift.start_time, shift.end_time))
.reduce((sum, hours)=> sum+hours, 0);
const overtime = Math.max(0, total - this.weeklyMax);
this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`);
return overtime;
}
//apply modifier to overtime hours
calculateOvertimePay(overtimeHours: number, modifier: number): number {
const pay = overtimeHours * modifier;
this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtimeHours}, modifier ${modifier})`);
return pay;
}
}

View File

@ -0,0 +1,61 @@
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
@Injectable()
export class SickLeaveService {
constructor(private readonly prisma: PrismaService) {}
private readonly logger = new Logger(SickLeaveService.name);
async calculateSickLeavePay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
//sets the year to jan 1st to dec 31st
const periodStart = new Date(startDate.getFullYear(), 0, 1);
const periodEnd = startDate;
//fetches all shifts of a selected employee
const shifts = await this.prisma.shifts.findMany({
where: {
timesheet: { employee_id: employeeId },
date: { gte: periodStart, lte: periodEnd},
},
select: { date: true },
});
//count the amount of worked days
const workedDates = new Set(
shifts.map(shift => shift.date.toISOString().slice(0,10))
);
const daysWorked = workedDates.size;
this.logger.debug(`Sick leave: days worked= ${daysWorked} in ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`);
//less than 30 worked days returns 0
if (daysWorked < 30) {
return 0;
}
//default 3 days allowed after 30 worked days
let acquiredDays = 3;
//identify the date of the 30th worked day
const orderedDates = Array.from(workedDates).sort();
const thresholdDate = new Date(orderedDates[29]); // index 29 is the 30th day
//calculate each completed month, starting the 1st of the next month
const firstBonusDate = new Date(thresholdDate.getFullYear(), thresholdDate.getMonth() +1, 1);
let months = (periodEnd.getFullYear() - firstBonusDate.getFullYear()) * 12 +
(periodEnd.getMonth() - firstBonusDate.getMonth()) + 1;
if(months < 0) months = 0;
acquiredDays += months;
//cap of 10 days
if (acquiredDays > 10) acquiredDays = 10;
this.logger.debug(`Sick leave: threshold Date = ${thresholdDate.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquiredDays}`);
const payableDays = Math.min(acquiredDays, daysRequested);
const rawHours = payableDays * 8 * modifier;
const rounded = Math.round(rawHours * 4) / 4;
this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`);
return rounded;
}
}

View File

@ -0,0 +1,85 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
@Injectable()
export class VacationService {
constructor(private readonly prisma: PrismaService) {}
private readonly logger = new Logger(VacationService.name);
/**
* Calculate the ammount allowed for vacation days.
*
* @param employeeId employee ID
* @param startDate first day of vacation
* @param daysRequested number of days requested
* @param modifier Coefficient of hours(1)
* @returns amount of payable hours
*/
async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
//fetch hiring date
const employee = await this.prisma.employees.findUnique({
where: { id: employeeId },
select: { first_work_day: true },
});
if(!employee) {
throw new NotFoundException(`Employee #${employeeId} not found`);
}
const hireDate = employee.first_work_day;
//sets "year" to may 1st to april 30th
//check if hiring date is in may or later, we use hiring year, otherwise we use the year before
const yearOfRequest = startDate.getMonth() >= 4
? startDate.getFullYear() : startDate.getFullYear() -1;
const periodStart = new Date(yearOfRequest, 4, 1); //may = 4
const periodEnd = new Date(yearOfRequest + 1, 4, 0); //day 0 of may == april 30th
this.logger.debug(`Vacation period for request: ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`);
//steps to reach to get more vacation weeks in years
const checkpoint = [5, 10, 15];
const anniversaries = checkpoint.map(years => {
const anniversaryDate = new Date(hireDate);
anniversaryDate.setFullYear(anniversaryDate.getFullYear() + years);
return anniversaryDate;
}).filter(d => d>= periodStart && d <= periodEnd).sort((a,b) => a.getTime() - b.getTime());
this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`);
const boundaries = [periodStart, ...anniversaries,periodEnd];
//calculate prorata per segment
let totalVacationDays = 0;
const msPerDay = 1000 * 60 * 60 * 24;
for (let i = 0; i < boundaries.length -1; i++) {
const segmentStart = boundaries[i];
const segmentEnd = boundaries[i+1];
//number of days in said segment
const daysInSegment = Math.round((segmentEnd.getTime() - segmentStart.getTime())/ msPerDay);
const yearsSinceHire = (segmentStart.getFullYear() - hireDate.getFullYear()) -
(segmentStart < new Date(segmentStart.getFullYear(), hireDate.getMonth()) ? 1 : 0);
let allocDays: number;
if(yearsSinceHire < 5) allocDays = 10;
else if(yearsSinceHire < 10) allocDays = 15;
else if(yearsSinceHire < 15) allocDays = 20;
else allocDays = 25;
//prorata for said segment
const prorata = (allocDays / 365) * daysInSegment;
totalVacationDays += prorata;
}
//compares allowed vacation pools with requested days
const payableDays = Math.min(totalVacationDays, daysRequested);
const rawHours = payableDays * 8 * modifier;
const roundedHours = Math.round(rawHours * 4) / 4;
this.logger.debug(`Vacation pay: entitledDays=${totalVacationDays.toFixed(2)}, requestedDays=${daysRequested},
payableDays=${payableDays.toFixed(2)}, hours=${roundedHours}`);
return roundedHours;
}
}

View File

@ -1,10 +1,10 @@
import { Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common';
import { Body,Controller,Delete,Get,NotFoundException,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common';
import { Employees, Roles as RoleEnum } from '@prisma/client';
import { EmployeesService } from '../services/employees.service';
import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
import { RolesAllowed } from '../../../common/decorators/roles.decorators';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard';
import { EmployeeEntity } from '../dtos/swagger-entities/employees.entity';
@ -27,7 +27,7 @@ export class EmployeesController {
@Get()
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
@ApiOperation({summary: 'Find all employees' })
@ApiResponse({ status: 201, description: 'List of employees found', type: EmployeeEntity, isArray: true })
@ApiResponse({ status: 200, description: 'List of employees found', type: EmployeeEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of employees not found' })
findAll(): Promise<Employees[]> {
return this.employeesService.findAll();
@ -36,30 +36,39 @@ export class EmployeesController {
@Get(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
@ApiOperation({summary: 'Find employee' })
@ApiResponse({ status: 201, description: 'Employee found', type: EmployeeEntity })
@ApiResponse({ status: 200, description: 'Employee found', type: EmployeeEntity })
@ApiResponse({ status: 400, description: 'Employee not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
@ApiOperation({summary: 'Update employee' })
@ApiResponse({ status: 201, description: 'Employee updated', type: EmployeeEntity })
@ApiResponse({ status: 400, description: 'Employee not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateEmployeeDto,
): Promise<Employees> {
return this.employeesService.update(id, dto);
}
@Delete(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
@ApiOperation({summary: 'Delete employee' })
@ApiResponse({ status: 201, description: 'Employee deleted', type: EmployeeEntity })
@ApiResponse({ status: 400, description: 'Employee not found' })
@ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee to delete' })
@ApiResponse({ status: 204, description: 'Employee deleted' })
@ApiResponse({ status: 404, description: 'Employee not found' })
remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
return this.employeesService.remove(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiBearerAuth('access-token')
@ApiOperation({ summary: 'Update, archive or restore an employee' })
@ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee' })
@ApiResponse({ status: 200, description: 'Employee updated or restored', type: EmployeeEntity })
@ApiResponse({ status: 202, description: 'Employee archived successfully', type: EmployeeEntity })
@ApiResponse({ status: 404, description: 'Employee not found in active or archive' })
@Patch(':id')
async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) {
// if last_work_day is set => archive the employee
// else if employee is archived and first_work_day or last_work_day = null => restore
//otherwise => standard update
const result = await this.employeesService.patchEmployee(+id, dto);
if(!result) {
throw new NotFoundException(`Employee #${ id } not found in active or archive.`)
}
return result;
}
}

View File

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

View File

@ -5,5 +5,6 @@ import { EmployeesService } from './services/employees.service';
@Module({
controllers: [EmployeesController],
providers: [EmployeesService],
exports: [EmployeesService],
})
export class EmployeesModule {}

View File

@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
import { Employees, Users } from '@prisma/client';
import { Employees, EmployeesArchive, Users } from '@prisma/client';
@Injectable()
export class EmployeesService {
@ -107,4 +107,98 @@ async update(
await this.findOne(id);
return this.prisma.employees.delete({ where: { id } });
}
//archivation functions ******************************************************
async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<any> {
//fetching existing employee
const existing = await this.prisma.employees.findUnique({
where: { id },
include: { user: true, archive: true },
});
if (existing) {
//verify last_work_day is not null => trigger archivation
if(dto.last_work_day != undefined && existing.last_work_day == null) {
return this.archiveOnTermination(existing, dto);
}
//if null => regular update
return this.prisma.employees.update({
where: { id },
data: dto,
});
}
//if not found => fetch archives side for restoration
const archived = await this.prisma.employeesArchive.findFirst({
where: { employee_id: id },
include: { employee: true, user: true },
});
if (archived) {
//conditions for restoration
const restore = dto.last_work_day === null || dto.first_work_day != null;
if(restore) {
return this.restoreEmployee(archived, dto);
}
}
//if neither activated nor archivated => 404
return null;
}
//transfers the employee to archive and then delete from employees table
private async archiveOnTermination(existing: any, dto: UpdateEmployeeDto): Promise<any> {
return this.prisma.$transaction(async transaction => {
//archive insertion
const archived = await transaction.employeesArchive.create({
data: {
employee_id: existing.id,
user_id: existing.user_id,
first_name: existing.first_name,
last_name: existing.last_name,
external_payroll_id: existing.external_payroll_id,
company_code: existing.company_code,
first_Work_Day: existing.first_Work_Day,
last_work_day: existing.last_work_day,
supervisor_id: existing.supervisor_id ?? null,
},
});
//delete from employees table
await transaction.employees.delete({ where: { id: existing.id } });
//return archived employee
return archived
});
}
//transfers the employee from archive to the employees table
private async restoreEmployee(archived: any, dto: UpdateEmployeeDto): Promise<any> {
return this.prisma.$transaction(async transaction => {
//restores the archived employee into the employees table
const restored = await transaction.employees.create({
data: {
id: archived.employee_id,
user_id: archived.user_id,
external_payroll_id: dto.external_payroll_id ?? archived.external_payroll_id,
company_code: dto.company_code ?? archived.company_code,
first_work_day: dto.first_work_day ?? archived.first_Work_Day,
last_work_day: null,
supervisor_id: dto.supervisor_id ?? archived.supervisor_id,
},
});
//deleting archived entry by id
await transaction.employeesArchive.delete({ where: { id: archived.id } });
//return restored employee
return restored;
});
}
//fetches all archived employees
async findAllArchived(): Promise<EmployeesArchive[]> {
return this.prisma.employeesArchive.findMany();
}
//fetches an archived employee
async findOneArchived(id: number): Promise<EmployeesArchive> {
return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } });
}
}

View File

@ -1,64 +0,0 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { ExpenseCodesService } from "../services/expense-codes.service";
import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ExpenseCodes } from "@prisma/client";
import { Roles as RoleEnum } from '.prisma/client';
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { CreateExpenseCodeDto } from "../dtos/create-expense-code";
import { ExpenseCodesEntity } from "../dtos/swagger-entities/expense-codes.entity";
import { UpdateExpenseCodeDto } from "../dtos/update-expense-code";
@ApiTags('Expense Codes')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('expense-codes')
export class ExpenseCodesController {
constructor(private readonly expenseCodesService: ExpenseCodesService) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Create expense code' })
@ApiResponse({ status: 201, description: 'Expense code created',type: ExpenseCodesEntity })
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
create(@Body()dto: CreateExpenseCodeDto): Promise<ExpenseCodes> {
return this.expenseCodesService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Find all expense codes' })
@ApiResponse({ status: 201, description: 'List of expense codes found',type: ExpenseCodesEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of expense codes not found' })
findAll(): Promise<ExpenseCodes[]> {
return this.expenseCodesService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Find expense code' })
@ApiResponse({ status: 201, description: 'Expense code found',type: ExpenseCodesEntity })
@ApiResponse({ status: 400, description: 'Expense code not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<ExpenseCodes> {
return this.expenseCodesService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Update expense code' })
@ApiResponse({ status: 201, description: 'Expense code updated',type: ExpenseCodesEntity })
@ApiResponse({ status: 400, description: 'Expense code not found' })
update(@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateExpenseCodeDto): Promise<ExpenseCodes> {
return this.expenseCodesService.update(id,dto);
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Delete expense code' })
@ApiResponse({ status: 201, description: 'Expense code deleted',type: ExpenseCodesEntity })
@ApiResponse({ status: 400, description: 'Expense code not found' })
remove(@Param('id', ParseIntPipe)id: number): Promise<ExpenseCodes> {
return this.expenseCodesService.remove(id);
}
}

View File

@ -1,21 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
export class CreateExpenseCodeDto {
@ApiProperty({
example:'mileage, overnight, etc...',
description: 'Type of expenses for an account perception',
})
@IsString()
@IsNotEmpty()
expense_type: string;
@ApiProperty({
example: 'G500, G501, etc...',
description: 'bank`s code related to the type of expense',
})
@IsString()
@IsNotEmpty()
bank_code: string;
}

View File

@ -1,21 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
export class ExpenseCodesEntity {
@ApiProperty({
example: 1,
description: 'Unique ID of a expense-code (auto-generated)',
})
id: number;
@ApiProperty({
example: 'Mileage',
description: 'Type of expenses for an account perception',
})
shift_type: string;
@ApiProperty({
example: 'G501',
description: 'bank`s code related to the type of expense',
})
bank_code: string;
}

View File

@ -1,4 +0,0 @@
import { PartialType } from "@nestjs/swagger";
import { CreateExpenseCodeDto } from "./create-expense-code";
export class UpdateExpenseCodeDto extends PartialType(CreateExpenseCodeDto) {}

View File

@ -1,11 +0,0 @@
import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { ExpenseCodesController } from "./controllers/expense-codes.controller";
import { ExpenseCodesService } from "./services/expense-codes.service";
@Module({
controllers: [ExpenseCodesController],
providers: [ExpenseCodesService, PrismaService]
})
export class ExpenseCodesModule {}

View File

@ -1,99 +0,0 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateExpenseCodeDto } from "../dtos/create-expense-code";
import { ExpenseCodes } from "@prisma/client";
import { UpdateExpenseCodeDto } from "../dtos/update-expense-code";
@Injectable()
export class ExpenseCodesService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateExpenseCodeDto): Promise<ExpenseCodes> {
const { expense_type, bank_code } = dto;
return this.prisma.expenseCodes.create({
data: { expense_type, bank_code },
include: {
expense: {
include: {
timesheet: {
include: {
employee: { include: { user: true } },
},
},
},
},
},
});
}
findAll(): Promise<ExpenseCodes[]> {
return this.prisma.expenseCodes.findMany({
include: {
expense: {
include: {
timesheet: {
include: {
employee: {
include: { user: true }
},
},
},
},
},
},
});
}
async findOne(id: number): Promise<ExpenseCodes> {
const record = await this.prisma.expenseCodes.findUnique({
where: { id },
include: {
expense: {
include: {
timesheet: {
include: {
employee: {
include: { user:true },
},
},
},
},
},
},
});
if(!record) {
throw new NotFoundException(`ExpenseCode #${id} not found`);
}
return record;
}
async update(id: number, dto: UpdateExpenseCodeDto): Promise<ExpenseCodes> {
await this.findOne(id);
const { expense_type, bank_code } = dto;
return this.prisma.expenseCodes.update({
where: { id },
data: {
...(expense_type !== undefined && { expense_type }),
...(bank_code !== undefined && { bank_code }),
},
include: {
expense: {
include: {
timesheet: {
include: {
employee: {
include: { user: true },
},
},
},
},
},
},
});
}
async remove(id: number): Promise<ExpenseCodes> {
await this.findOne(id);
return this.prisma.expenseCodes.delete({ where: { id } });
}
}

View File

@ -1,61 +1,32 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsBoolean, IsDate, IsDateString, IsInt, IsOptional, IsString } from "class-validator";
export class CreateExpenseDto {
@ApiProperty({
example: 'Th3F3110w5h1pX2024',
description: 'ID number for a set timesheet',
})
@Type(()=> Number)
@IsInt()
timesheet_id: number;
@ApiProperty({
example: '0n3R1n962Ru13xX',
description: 'ID number of an expense code (link with shift-codes)',
})
@Type(() => Number)
@IsInt()
expense_code_id: number;
bank_code_id: number;
@ApiProperty({
example: '20/10/3018',
description: 'Date where the expense was made',
})
@IsDateString()
@Type(() => Date)
@IsDate()
date: Date;
@ApiProperty({
example: '280 000 000,00',
description: 'Amount of the expense',
})
@Type(() => Number)
@IsInt()
amount: number
@ApiProperty({
example:'Spent for mileage between A and B',
description:'explain`s why the expense was made'
})
@IsString()
description?: string;
@ApiProperty({
example: 'True or False or Pending or Denied or Cancelled or Escalated',
description: 'Expense`s approval status',
})
@IsOptional()
@IsBoolean()
is_approved?: boolean;
@ApiProperty({
example:'Asked X to go there as an emergency response',
description:'Supervisro`s justification for the spending of an employee'
})
@IsString()
supervisor_comment?: string;
}

View File

@ -16,9 +16,9 @@ export class ExpenseEntity {
@ApiProperty({
example: 7,
description: 'ID number of an expense code (link with expense-codes)',
description: 'ID number of an bank code (link with bank-codes)',
})
expense: number;
bank_code_id: number;
@ApiProperty({
example: '3018-10-20T00:00:00.000Z',

View File

@ -2,10 +2,13 @@ import { PrismaService } from "src/prisma/prisma.service";
import { ExpensesController } from "./controllers/expenses.controller";
import { Module } from "@nestjs/common";
import { ExpensesService } from "./services/expenses.service";
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
@Module({
imports: [BusinessLogicsModule],
controllers: [ExpensesController],
providers: [ExpensesService, PrismaService]
providers: [ExpensesService],
exports: [ ExpensesService ],
})
export class ExpensesModule {}

View File

@ -1,51 +1,58 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateExpenseDto } from "../dtos/create-expense";
import { Expenses } from "@prisma/client";
import { Expenses, ExpensesArchive } from "@prisma/client";
import { UpdateExpenseDto } from "../dtos/update-expense";
import { MileageService } from "src/modules/business-logics/services/mileage.service";
@Injectable()
export class ExpensesService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly mileageService: MileageService,
) {}
async create(dto: CreateExpenseDto): Promise<Expenses> {
const { timesheet_id, expense_code_id, date, amount,
const { timesheet_id, bank_code_id, date, amount:rawAmount,
description, is_approved,supervisor_comment} = dto;
return this.prisma.expenses.create({
data: { timesheet_id, expense_code_id,date,amount,description,is_approved,supervisor_comment},
include: {
timesheet: {
include: {
employee: { include: { user: true } },
},
},
expense_code: true,
},
//fetches type and modifier
const bankCode = await this.prisma.bankCodes.findUnique({
where: { id: bank_code_id },
select: { type: true, modifier: true },
});
if(!bankCode) {
throw new NotFoundException(`bank_code #${bank_code_id} not found`)
}
//if mileage -> service, otherwise the ratio is amount:1
let finalAmount: number;
if(bankCode.type === 'mileage') {
finalAmount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id);
}else {
finalAmount = parseFloat( (rawAmount * bankCode.modifier).toFixed(2));
}
return this.prisma.expenses.create({
data: { timesheet_id, bank_code_id, date, amount: finalAmount, description, is_approved, supervisor_comment},
include: { timesheet: { include: { employee: { include: { user: true }}}},
bank_code: true,
},
})
}
findAll(): Promise<Expenses[]> {
return this.prisma.expenses.findMany({
include: {
timesheet: {
include: {
employee: { include: { user: true } },
},
},
},
include: { timesheet: { include: { employee: { include: { user: true } } } } },
});
}
async findOne(id: number): Promise<Expenses> {
const expense = await this.prisma.expenses.findUnique({
where: { id },
include: {
timesheet: {
include: {
employee: { include: { user:true } },
},
},
expense_code: true,
include: { timesheet: { include: { employee: { include: { user:true } } } },
bank_code: true,
},
});
if (!expense) {
@ -56,28 +63,21 @@ export class ExpensesService {
async update(id: number, dto: UpdateExpenseDto): Promise<Expenses> {
await this.findOne(id);
const { timesheet_id, expense_code_id, date, amount,
const { timesheet_id, bank_code_id, date, amount,
description, is_approved, supervisor_comment} = dto;
return this.prisma.expenses.update({
where: { id },
data: {
...(timesheet_id !== undefined && { timesheet_id}),
...(expense_code_id !== undefined && { expense_code_id }),
...(bank_code_id !== undefined && { bank_code_id }),
...(date !== undefined && { date }),
...(amount !== undefined && { amount }),
...(description !== undefined && { description }),
...(is_approved !== undefined && { is_approved }),
...(supervisor_comment !== undefined && { supervisor_comment }),
},
include: {
timesheet: {
include: {
employee: {
include: { user: true }
},
},
},
expense_code: true,
include: { timesheet: { include: { employee: { include: { user: true } } } },
bank_code: true,
},
});
}
@ -85,5 +85,62 @@ export class ExpensesService {
async remove(id: number): Promise<Expenses> {
await this.findOne(id);
return this.prisma.expenses.delete({ where: { id } });
}
}
//archivation functions ******************************************************
async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true },
});
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) {
return;
}
// copy/delete transaction
await this.prisma.$transaction(async transaction => {
//fetches expenses to move to archive
const expensesToArchive = await transaction.expenses.findMany({
where: { timesheet_id: { in: timesheetIds } },
});
if(expensesToArchive.length === 0) {
return;
}
//copies sent to archive table
await transaction.expensesArchive.createMany({
data: expensesToArchive.map(exp => ({
expense_id: exp.id,
timesheet_id: exp.timesheet_id,
bank_code_id: exp.bank_code_id,
date: exp.date,
amount: exp.amount,
attachement: exp.attachement,
description: exp.description,
is_approved: exp.is_approved,
supervisor_comment: exp.supervisor_comment,
})),
});
//delete from expenses table
await transaction.expenses.deleteMany({
where: { id: { in: expensesToArchive.map(exp => exp.id) } },
})
})
}
//fetches all archived timesheets
async findAllArchived(): Promise<ExpensesArchive[]> {
return this.prisma.expensesArchive.findMany();
}
//fetches an archived timesheet
async findOneArchived(id: number): Promise<ExpensesArchive> {
return this.prisma.expensesArchive.findUniqueOrThrow({ where: { id } });
}
}

View File

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { LeaveRequestsService } from "../services/leave-request.service";
import { LeaveRequestsService } from "../services/leave-requests.service";
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
import { LeaveRequests } from "@prisma/client";
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";

View File

@ -12,6 +12,10 @@ export class CreateLeaveRequestsDto {
@IsInt()
employee_id: number;
@Type(()=> Number)
@IsInt()
bank_code_id: number;
@ApiProperty({
example: 'Sick or Vacation or Unpaid or Bereavement or Parental or Legal',
description: 'type of leave request for an accounting perception',

View File

@ -1,11 +1,13 @@
import { PrismaService } from "src/prisma/prisma.service";
import { LeaveRequestController } from "./controllers/leave-requests.controller";
import { LeaveRequestsService } from "./services/leave-request.service";
import { LeaveRequestsService } from "./services/leave-requests.service";
import { Module } from "@nestjs/common";
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
@Module({
imports: [BusinessLogicsModule],
controllers: [LeaveRequestController],
providers: [ LeaveRequestsService, PrismaService],
providers: [LeaveRequestsService],
exports: [LeaveRequestsService],
})
export class LeaveRequestsModule {}

View File

@ -1,109 +0,0 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
import { LeaveRequests } from "@prisma/client";
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
@Injectable()
export class LeaveRequestsService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
const {
employee_id,
leave_type,
start_date_time,
end_date_time,
comment,
approval_status,
} = dto;
return this.prisma.leaveRequests.create({
data: {
employee_id,
leave_type,
start_date_time,
end_date_time,
comment,
approval_status: approval_status ?? undefined,
},
include: {
employee: {
include: {
user: true
},
},
},
});
}
findAll(): Promise<LeaveRequests[]> {
return this.prisma.leaveRequests.findMany({
include: {
employee: {
include: {
user: true
},
},
},
});
}
async findOne(id:number): Promise<LeaveRequests> {
const req = await this.prisma.leaveRequests.findUnique({
where: { id },
include: {
employee: {
include: {
user: true
},
},
},
});
if(!req) {
throw new NotFoundException(`LeaveRequest #${id} not found`);
}
return req;
}
async update(
id: number,
dto: UpdateLeaveRequestsDto,
): Promise<LeaveRequests> {
await this.findOne(id);
const {
employee_id,
leave_type,
start_date_time,
end_date_time,
comment,
approval_status,
} = dto;
return this.prisma.leaveRequests.update({
where: { id },
data: {
...(employee_id !== undefined && { employee_id }),
...(leave_type !== undefined && { leave_type } ),
...(start_date_time !== undefined && { start_date_time }),
...(end_date_time !== undefined && { end_date_time }),
...(comment !== undefined && { comment }),
...(approval_status == undefined && { approval_status }),
},
include: {
employee: {
include: {
user:true
},
},
},
});
}
async remove(id:number): Promise<LeaveRequests> {
await this.findOne(id);
return this.prisma.leaveRequests.delete({
where: { id },
});
}
}

View File

@ -0,0 +1,183 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
import { VacationService } from "src/modules/business-logics/services/vacation.service";
@Injectable()
export class LeaveRequestsService {
constructor(
private readonly prisma: PrismaService,
private readonly holidayService: HolidayService,
private readonly vacationService: VacationService,
private readonly sickLeaveService: SickLeaveService
) {}
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
const { employee_id, bank_code_id, leave_type, start_date_time,
end_date_time, comment, approval_status } = dto;
return this.prisma.leaveRequests.create({
data: { employee_id, bank_code_id, leave_type, start_date_time,
end_date_time, comment, approval_status: approval_status ?? undefined
},
include: { employee: { include: { user: true } },
bank_code: true
},
});
}
async findAll(): Promise<any[]> {
const list = await this.prisma.leaveRequests.findMany({
include: { employee: { include: { user: true } },
bank_code: true,
},
});
const msPerDay = 1000 * 60 * 60 * 24;
return Promise.all(
list.map(async request => {
// end_date fallback
const endDate = request.end_date_time ?? request.start_date_time;
//Requested days
const diffDays = Math.round((endDate.getTime() - request.start_date_time.getTime()) / msPerDay) +1;
// modifier fallback/validation
if (!request.bank_code || request.bank_code.modifier == null) {
throw new BadRequestException(`Modifier manquant pour bank_code_id=${request.bank_code_id}`);
}
const modifier = request.bank_code.modifier;
let cost: number;
switch (request.bank_code.type) {
case 'holiday' :
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
break;
case 'vacation' :
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diffDays, modifier );
break;
case 'sick' :
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier );
break;
default:
cost = diffDays * modifier;
}
return {...request, daysRequested: diffDays, cost };
})
);
}
async findOne(id:number): Promise<any> {
const request = await this.prisma.leaveRequests.findUnique({
where: { id },
include: { employee: { include: { user: true } },
bank_code: true,
},
});
if(!request) {
throw new NotFoundException(`LeaveRequest #${id} not found`);
}
//validation and fallback for end_date_time
const endDate = request.end_date_time ?? request.start_date_time;
//calculate included days
const msPerDay = 1000 * 60 * 60 * 24;
const diffDays = Math.floor((endDate.getTime() - request.start_date_time.getTime())/ msPerDay) + 1;
if (!request.bank_code || request.bank_code.modifier == null) {
throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`);
}
const modifier = request.bank_code.modifier;
//calculate cost based on bank_code types
let cost = diffDays * modifier;
switch(request.bank_code.type) {
case 'holiday':
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
break;
case 'vacation':
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diffDays, modifier );
break;
case 'sick':
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier );
break;
default:
cost = diffDays * modifier;
}
return {...request, daysRequested: diffDays, cost };
}
async update(
id: number,
dto: UpdateLeaveRequestsDto,
): Promise<LeaveRequests> {
await this.findOne(id);
const { employee_id, leave_type, start_date_time, end_date_time, comment, approval_status } = dto;
return this.prisma.leaveRequests.update({
where: { id },
data: {
...(employee_id !== undefined && { employee_id }),
...(leave_type !== undefined && { leave_type } ),
...(start_date_time !== undefined && { start_date_time }),
...(end_date_time !== undefined && { end_date_time }),
...(comment !== undefined && { comment }),
...(approval_status !== undefined && { approval_status }),
},
include: { employee: { include: { user:true } } },
});
}
async remove(id:number): Promise<LeaveRequests> {
await this.findOne(id);
return this.prisma.leaveRequests.delete({
where: { id },
});
}
//archivation functions ******************************************************
async archiveExpired(): Promise<void> {
const now = new Date();
await this.prisma.$transaction(async transaction => {
//fetches expired leave requests
const expired = await transaction.leaveRequests.findMany({
where: { end_date_time: { lt: now } },
});
if(expired.length === 0) {
return;
}
//copy unto archive table
await transaction.leaveRequestsArchive.createMany({
data: expired.map(request => ({
leave_request_id: request.id,
employee_id: request.employee_id,
leave_type: request.leave_type,
start_date_time: request.start_date_time,
end_date_time: request.end_date_time,
comment: request.comment,
approval_status: request.approval_status,
})),
});
//delete from leave_requests table
await transaction.leaveRequests.deleteMany({
where: { id: { in: expired.map(request => request.id ) } },
});
});
}
//fetches all archived employees
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
return this.prisma.leaveRequestsArchive.findMany();
}
//fetches an archived employee
async findOneArchived(id: number): Promise<LeaveRequestsArchive> {
return this.prisma.leaveRequestsArchive.findUniqueOrThrow({ where: { id } });
}
}

View File

@ -1,64 +0,0 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { ShiftCodesService } from "../services/shift-codes.service";
import { CreateShiftCodeDto } from "../dtos/create-shift-codes.dto";
import { ShiftCodes } from "@prisma/client";
import { UpdateShiftCodeDto } from "../dtos/update-shift-codes.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ShiftCodesEntity } from "../dtos/swagger-entities/shift-codes.entity";
@ApiTags('Shift Codes')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('shift-codes')
export class ShiftCodesController {
constructor(private readonly shiftCodesService: ShiftCodesService) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Create shift code' })
@ApiResponse({ status: 201, description: 'Shift code created',type: ShiftCodesEntity })
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
create(@Body()dto: CreateShiftCodeDto): Promise<ShiftCodes> {
return this.shiftCodesService.create(dto);
}
@Get()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Find all shift codes' })
@ApiResponse({ status: 201, description: 'List of shift codes found',type: ShiftCodesEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of shift codes not found' })
findAll(): Promise<ShiftCodes[]> {
return this.shiftCodesService.findAll();
}
@Get(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Find shift code' })
@ApiResponse({ status: 201, description: 'Shift code found',type: ShiftCodesEntity })
@ApiResponse({ status: 400, description: 'Shift code not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<ShiftCodes> {
return this.shiftCodesService.findOne(id);
}
@Patch(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Update shift code' })
@ApiResponse({ status: 201, description: 'Shift code updated',type: ShiftCodesEntity })
@ApiResponse({ status: 400, description: 'Shift code not found' })
update(@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateShiftCodeDto): Promise<ShiftCodes> {
return this.shiftCodesService.update(id,dto);
}
@Delete(':id')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR)
@ApiOperation({ summary: 'Delete shift code' })
@ApiResponse({ status: 201, description: 'Shift code deleted',type: ShiftCodesEntity })
@ApiResponse({ status: 400, description: 'Shift code not found' })
remove(@Param('id', ParseIntPipe)id: number): Promise<ShiftCodes> {
return this.shiftCodesService.remove(id);
}
}

View File

@ -1,21 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
export class CreateShiftCodeDto {
@ApiProperty({
example: 'Regular or Night or Emergency, etc...',
description: 'Type of shifts for an account perception',
})
@IsString()
@IsNotEmpty()
shift_type: string;
@ApiProperty({
example: 'G1, G2, G3, etc...',
description: 'bank`s code related to the type of shift',
})
@IsString()
@IsNotEmpty()
bank_code: string;
}

View File

@ -1,21 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class ShiftCodesEntity {
@ApiProperty({
example: 1,
description: 'Unique ID of a shift-code (auto-generated)',
})
id: number;
@ApiProperty({
example: 'Night',
description: 'Type of shifts for an account perception',
})
shift_type: string;
@ApiProperty({
example: 'G2',
description: 'bank`s code related to the type of shift',
})
bank_code: string;
}

View File

@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger';
import { CreateShiftCodeDto } from './create-shift-codes.dto';
export class UpdateShiftCodeDto extends PartialType(CreateShiftCodeDto) {}

View File

@ -1,109 +0,0 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateShiftCodeDto } from "../dtos/create-shift-codes.dto";
import { ShiftCodes } from "@prisma/client";
import { UpdateShiftCodeDto } from "../dtos/update-shift-codes.dto";
@Injectable()
export class ShiftCodesService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateShiftCodeDto): Promise<ShiftCodes> {
const { shift_type, bank_code } = dto;
return this.prisma.shiftCodes.create({
data: { shift_type, bank_code },
include: {
shift: {
include: {
timesheet: {
include: {
employee: {
include: {
user: true,
},
},
},
},
},
},
},
});
}
findAll(): Promise<ShiftCodes[]> {
return this.prisma.shiftCodes.findMany({
include: {
shift: {
include: {
timesheet: {
include: {
employee: {
include: {
user: true
}
}
}
}
},
},
},
});
}
async findOne(id: number): Promise<ShiftCodes> {
const record = await this.prisma.shiftCodes.findUnique({
where: { id },
include: {
shift: {
include: {
timesheet: {
include: {
employee: {
include: {
user:true,
}
}
}
}
}
}
}
});
if(!record) {
throw new NotFoundException(`ShiftCode #${id} not found`);
}
return record;
}
async update(id: number, dto: UpdateShiftCodeDto): Promise<ShiftCodes> {
await this.findOne(id);
const { shift_type, bank_code } = dto;
return this.prisma.shiftCodes.update({
where: { id },
data: {
...(shift_type !== undefined && { shift_type }),
...(bank_code !== undefined && { bank_code }),
},
include: {
shift: {
include: {
timesheet: {
include: {
employee: {
include: {
user: true
}
}
}
},
},
},
},
});
}
async remove(id:number): Promise<ShiftCodes>{
await this.findOne(id);
return this.prisma.shiftCodes.delete({ where: { id }});
}
}

View File

@ -1,11 +0,0 @@
import { Module } from "@nestjs/common";
import { ShiftCodesController } from "./controllers/shift-codes.controller";
import { ShiftCodesService } from "./services/shift-codes.service";
import { PrismaService } from "src/prisma/prisma.service";
@Module({
controllers: [ShiftCodesController],
providers: [ShiftCodesService, PrismaService],
})
export class ShiftCodesModule {}

View File

@ -1,56 +1,31 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsDate, IsDateString, IsInt, IsString } from "class-validator";
export class CreateShiftDto {
@ApiProperty({
example: 'Th3F3110w5h1pX2024',
description: 'ID number for a set timesheet',
})
@Type(() => Number)
@IsInt()
timesheet_id: number;
@ApiProperty({
example: '0n3R1n962Ru13xX',
description: 'ID number of a shift code (link with shift-codes)',
})
@Type(() => Number)
@IsInt()
shift_code_id: number;
bank_code_id: number;
@ApiProperty({
example: '20/10/3018',
description: 'Date where the shift takes place',
})
@IsDateString()
@Type(() => Date)
@IsDate()
date: Date;
@ApiProperty({
example: '08:00',
description: 'Start time of the said shift',
})
@IsDateString()
@Type(() => Date)
@IsDate()
start_time: Date;
@ApiProperty({
example: '17:00',
description: 'End time of the said shift',
})
@IsDateString()
@Type(() => Date)
@IsDate()
end_time: Date;
@ApiProperty({
example:'Called for an emergency at X` place',
description:'justify the purpose of the shift'
})
@IsString()
description: string;
}

View File

@ -15,9 +15,9 @@ export class ShiftEntity {
@ApiProperty({
example: 7,
description: 'ID number of a shift code (link with shift-codes)',
description: 'ID number of a shift code (link with bank-codes)',
})
shift_code_id: number;
bank_code_id: number;
@ApiProperty({
example: '3018-10-20T00:00:00.000Z',

View File

@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateShiftDto } from "../dtos/create-shifts.dto";
import { Shifts } from "@prisma/client";
import { Shifts, ShiftsArchive } from "@prisma/client";
import { UpdateShiftsDto } from "../dtos/update-shifts.dto";
@Injectable()
@ -9,46 +9,26 @@ export class ShiftsService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateShiftDto): Promise<Shifts> {
const { timesheet_id, shift_code_id, date, start_time, end_time } = dto;
const { timesheet_id, bank_code_id, date, start_time, end_time } = dto;
return this.prisma.shifts.create({
data: { timesheet_id, shift_code_id, date, start_time, end_time },
include: {
timesheet: {
include: {
employee: { include: { user: true } },
},
},
shift_code: true,
data: { timesheet_id, bank_code_id, date, start_time, end_time },
include: { timesheet: { include: { employee: { include: { user: true } } } },
bank_code: true,
},
});
}
findAll(): Promise<Shifts[]> {
return this.prisma.shifts.findMany({
include: {
timesheet: {
include: {
employee: {
include: { user:true }
},
},
},
},
include: { timesheet: { include: { employee: { include: { user:true } } } } },
});
}
async findOne(id: number): Promise<Shifts> {
const shift = await this.prisma.shifts.findUnique({
where: { id },
include: {
timesheet: {
include: {
employee: {
include: { user: true }
},
},
},
shift_code: true,
include: { timesheet: { include: { employee: { include: { user: true } } } },
bank_code: true,
},
});
if(!shift) {
@ -59,25 +39,18 @@ export class ShiftsService {
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
await this.findOne(id);
const { timesheet_id, shift_code_id, date,start_time,end_time} = dto;
const { timesheet_id, bank_code_id, date,start_time,end_time} = dto;
return this.prisma.shifts.update({
where: { id },
data: {
...(timesheet_id !== undefined && { timesheet_id }),
...(shift_code_id !== undefined && { shift_code_id }),
...(bank_code_id !== undefined && { bank_code_id }),
...(date !== undefined && { date }),
...(start_time !== undefined && { start_time }),
...(end_time !== undefined && { end_time }),
},
include: {
timesheet: {
include: {
employee: {
include: { user: true }
},
},
},
shift_code: true,
include: { timesheet: { include: { employee: { include: { user: true } } } },
bank_code: true,
},
});
}
@ -86,4 +59,59 @@ export class ShiftsService {
await this.findOne(id);
return this.prisma.shifts.delete({ where: { id } });
}
//archivation functions ******************************************************
async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true },
});
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) {
return;
}
// copy/delete transaction
await this.prisma.$transaction(async transaction => {
//fetches shifts to move to archive
const shiftsToArchive = await transaction.shifts.findMany({
where: { timesheet_id: { in: timesheetIds } },
});
if(shiftsToArchive.length === 0) {
return;
}
//copies sent to archive table
await transaction.shiftsArchive.createMany({
data: shiftsToArchive.map(shift => ({
shift_id: shift.id,
timesheet_id: shift.timesheet_id,
bank_code_id: shift.bank_code_id,
description: shift.description ?? undefined,
date: shift.date,
start_time: shift.start_time,
end_time: shift.end_time,
})),
});
//delete from shifts table
await transaction.shifts.deleteMany({
where: { id: { in: shiftsToArchive.map(shift => shift.id) } },
})
})
}
//fetches all archived timesheets
async findAllArchived(): Promise<ShiftsArchive[]> {
return this.prisma.shiftsArchive.findMany();
}
//fetches an archived timesheet
async findOneArchived(id: number): Promise<ShiftsArchive> {
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
}
}

View File

@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { ShiftsController } from './controllers/shifts.controller';
import { ShiftsService } from './services/shifts.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
@Module({
imports: [BusinessLogicsModule],
controllers: [ShiftsController],
providers: [ShiftsService, PrismaService]
providers: [ShiftsService],
exports: [ShiftsService],
})
export class ShiftsModule {}

View File

@ -0,0 +1,51 @@
import { Controller, Get, Header, Query } from "@nestjs/common";
import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service";
import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto";
@Controller()
export class ShiftsValidationController {
constructor(private readonly shiftsValidationService: ShiftsValidationService) {}
@Get()
async getSummary( @Query() query: GetShiftsValidationDto): Promise<ValidationRow[]> {
return this.shiftsValidationService.getSummary(query.periodId);
}
@Get('export.csv')
@Header('Content-Type', 'text/csv; charset=utf-8')
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
async exportCsv(@Query() query: GetShiftsValidationDto): Promise<Buffer>{
const rows = await this.shiftsValidationService.getSummary(query.periodId);
//CSV Headers
const header = [
'fullName',
'supervisor',
'totalRegularHrs',
'totalEveningHrs',
'totalOvertimeHrs',
'totalExpenses',
'totalMileage',
'isValidated'
].join(',') + '\n';
//CSV rows
const body = rows.map(r => {
const esc = (str: string) => `"${str.replace(/"/g, '""')}"`;
return [
esc(r.fullName),
esc(r.supervisor),
r.totalRegularHrs.toFixed(2),
r.totalEveningHrs.toFixed(2),
r.totalOvertimeHrs.toFixed(2),
r.totalExpenses.toFixed(2),
r.totalMileage.toFixed(2),
r.isValidated,
].join(',');
}).join('\n');
return Buffer.from(header + body, 'utf8');
}
}

View File

@ -0,0 +1,10 @@
import { Type } from "class-transformer";
import { IsInt, Min, Max } from "class-validator";
export class GetShiftsValidationDto {
@Type(()=> Number)
@IsInt()
@Min(1)
@Max(26)
periodId: number;
}

View File

@ -0,0 +1,122 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
export interface ValidationRow {
fullName: string;
supervisor: string;
totalRegularHrs: number;
totalEveningHrs: number;
totalOvertimeHrs: number;
totalExpenses: number;
totalMileage: number;
isValidated: boolean;
}
@Injectable()
export class ShiftsValidationService {
constructor(private readonly prisma: PrismaService) {}
private computeHours(start: Date, end: Date): number {
const diffMs = end.getTime() - start.getTime();
const hours = diffMs / 1000 / 3600;
return parseFloat(hours.toFixed(2));
}
async getSummary(periodId: number): Promise<ValidationRow[]> {
//fetch pay-period to display
const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodId },
});
if(!period) {
throw new NotFoundException(`pay-period ${periodId} not found`);
}
const { start_date, end_date } = period;
//prepare shifts and expenses for display
const shifts = await this.prisma.shifts.findMany({
where: { date: { gte: start_date, lte: end_date } },
include: {
bank_code: true,
timesheet: { include: { employee: {
include: { user:true,
supervisor: { include: { user: true } },
} },
} },
},
});
const expenses = await this.prisma.expenses.findMany({
where: { date: { gte: start_date, lte: end_date } },
include: {
bank_code: true,
timesheet: { include: { employee: {
include: { user:true,
supervisor: { include: { user:true } },
} },
} },
},
});
const mapRow = new Map<string, ValidationRow>();
for(const s of shifts) {
const employeeId = s.timesheet.employee.user_id;
const user = s.timesheet.employee.user;
const sup = s.timesheet.employee.supervisor?.user;
let row = mapRow.get(employeeId);
if(!row) {
row = {
fullName: `${user.first_name} ${user.last_name}`,
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
totalRegularHrs: 0,
totalEveningHrs: 0,
totalOvertimeHrs: 0,
totalExpenses: 0,
totalMileage: 0,
isValidated: false,
};
}
const hours = this.computeHours(s.start_time, s.end_time);
switch(s.bank_code.type) {
case 'regular' : row.totalRegularHrs += hours;
break;
case 'evening' : row.totalEveningHrs += hours;
break;
case 'overtime' : row.totalOvertimeHrs += hours;
break;
default: row.totalRegularHrs += hours;
}
mapRow.set(employeeId, row);
}
for(const e of expenses) {
const employeeId = e.timesheet.employee.user_id;
const user = e.timesheet.employee.user;
const sup = e.timesheet.employee.supervisor?.user;
let row = mapRow.get(employeeId);
if(!row) {
row = {
fullName: `${user.first_name} ${user.last_name}`,
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
totalRegularHrs: 0,
totalEveningHrs: 0,
totalOvertimeHrs: 0,
totalExpenses: 0,
totalMileage: 0,
isValidated: false,
};
}
const amount = Number(e.amount);
row.totalExpenses += amount;
if(e.bank_code.type === 'mileage') {
row.totalMileage += amount;
}
mapRow.set(employeeId, row);
}
//return by default the list of employee in ascending alphabetical order
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
}
}

View File

@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { ShiftsValidationController } from "./controllers/shifts-validation.controller";
import { ShiftsValidationService } from "./services/shifts-validation.service";
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
@Module({
imports: [BusinessLogicsModule],
controllers: [ShiftsValidationController],
providers: [ShiftsValidationService],
})
export class ShiftsValidationModule {}

View File

@ -1,53 +1,82 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets } from '@prisma/client';
import { Timesheets, TimesheetsArchive } from '@prisma/client';
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
@Injectable()
export class TimesheetsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly overtime: OvertimeService,
) {}
async create(dto : CreateTimesheetDto): Promise<Timesheets> {
const { employee_id, is_approved } = dto;
return this.prisma.timesheets.create({
data: {
employee_id,
is_approved: is_approved ?? false,
},
data: { employee_id, is_approved: is_approved ?? false },
include: {
employee: {
include: { user: true }
employee: { include: { user: true }
},
},
});
}
findAll(): Promise<Timesheets[]> {
return this.prisma.timesheets.findMany({
include: {
employee: {
include: { user: true },
},
async findAll(): Promise<any[]> {
const list = await this.prisma.timesheets.findMany({
include: {
shift: { include: { bank_code: true } },
expense: { include: { bank_code: true } },
employee: { include: { user : true } },
},
});
return Promise.all(
list.map(async timesheet => {
const detailedShifts = timesheet.shift.map(s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
return { ...s, hours, payRegular, payOvertime };
});
const weeklyOvertimeHours = detailedShifts.length
? await this.overtime.getWeeklyOvertimeHours(
timesheet.employee_id,
timesheet.shift[0].date): 0;
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
})
);
}
async findOne(id: number): Promise<Timesheets> {
const record = await this.prisma.timesheets.findUnique({
async findOne(id: number): Promise<any> {
const timesheet = await this.prisma.timesheets.findUnique({
where: { id },
include: {
employee: {
include: {
user:true
}
},
},
include: {
shift: { include: { bank_code: true } },
expense: { include: { bank_code: true } },
employee: { include: { user: true } },
},
});
if(!record) {
if(!timesheet) {
throw new NotFoundException(`Timesheet #${id} not found`);
}
return record;
const detailedShifts = timesheet.shift.map( s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
return { ...s, hours, payRegular, payOvertime };
});
const weeklyOvertimeHours = detailedShifts.length
? await this.overtime.getWeeklyOvertimeHours(
timesheet.employee_id,
timesheet.shift[0].date): 0;
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
}
async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> {
@ -59,20 +88,61 @@ export class TimesheetsService {
...(employee_id !== undefined && { employee_id }),
...(is_approved !== undefined && { is_approved }),
},
include: {
employee: {
include: { user: true }
},
include: { employee: { include: { user: true } },
},
});
}
async remove(id: number): Promise<Timesheets> {
await this.findOne(id);
return this.prisma.timesheets.delete({
where: { id },
});
return this.prisma.timesheets.delete({ where: { id } });
}
//archivation functions ******************************************************
async archiveOld(): Promise<void> {
//calcul du cutoff pour archivation
const cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - 6)
await this.prisma.$transaction(async transaction => {
//fetches all timesheets to cutoff
const oldSheets = await transaction.timesheets.findMany({
where: { shift: { every: { date: { lt: cutoff } } },
},
select: {
id: true,
employee_id: true,
is_approved: true,
},
});
if( oldSheets.length === 0) {
return;
}
//preping data for archivation
const archiveDate = oldSheets.map(sheet => ({
timesheet_id: sheet.id,
employee_id: sheet.employee_id,
is_approved: sheet.is_approved,
}));
//copying data from timesheets table to archive table
await transaction.timesheetsArchive.createMany({ data: archiveDate });
//removing data from timesheets table
await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } });
});
}
//fetches all archived timesheets
async findAllArchived(): Promise<TimesheetsArchive[]> {
return this.prisma.timesheetsArchive.findMany();
}
//fetches an archived timesheet
async findOneArchived(id: number): Promise<TimesheetsArchive> {
return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } });
}
}

View File

@ -1,9 +1,12 @@
import { Module } from '@nestjs/common';
import { TimesheetsController } from './controllers/timesheets.controller';
import { TimesheetsService } from './services/timesheets.service';
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
@Module({
imports: [BusinessLogicsModule],
controllers: [TimesheetsController],
providers: [TimesheetsService]
providers: [ TimesheetsService ],
exports: [TimesheetsService],
})
export class TimesheetsModule {}