fix(merge): conflicts resolved

This commit is contained in:
Nicolas Drolet 2025-08-08 13:44:24 -04:00
commit f4c69c4620
42 changed files with 653 additions and 210 deletions

View File

@ -3,7 +3,7 @@
"paths": { "paths": {
"/": { "/": {
"get": { "get": {
"operationId": "ShiftsValidationController_getSummary", "operationId": "ShiftsOverviewController_getSummary",
"parameters": [], "parameters": [],
"responses": { "responses": {
"200": { "200": {
@ -11,7 +11,7 @@
} }
}, },
"tags": [ "tags": [
"ShiftsValidation" "ShiftsOverview"
] ]
} }
}, },
@ -739,6 +739,34 @@
] ]
} }
}, },
"/Expenses/{id}/approval": {
"patch": {
"operationId": "ExpensesController_approve",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"access-token": []
}
],
"tags": [
"Expenses"
]
}
},
"/shifts": { "/shifts": {
"post": { "post": {
"operationId": "ShiftsController_create", "operationId": "ShiftsController_create",
@ -933,9 +961,37 @@
] ]
} }
}, },
"/shifts/{id}/approval": {
"patch": {
"operationId": "ShiftsController_approve",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"access-token": []
}
],
"tags": [
"Shifts"
]
}
},
"/export.csv": { "/export.csv": {
"get": { "get": {
"operationId": "ShiftsValidationController_exportCsv", "operationId": "ShiftsOverviewController_exportCsv",
"parameters": [], "parameters": [],
"responses": { "responses": {
"200": { "200": {
@ -943,7 +999,7 @@
} }
}, },
"tags": [ "tags": [
"ShiftsValidation" "ShiftsOverview"
] ]
} }
}, },
@ -1288,6 +1344,29 @@
] ]
} }
}, },
"/exports/csv": {
"get": {
"operationId": "CsvExportController_exportCsv",
"parameters": [
{
"name": "period",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"CsvExport"
]
}
},
"/customers": { "/customers": {
"post": { "post": {
"operationId": "CustomersController_create", "operationId": "CustomersController_create",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "shifts" ADD COLUMN "is_approved" BOOLEAN NOT NULL DEFAULT false;

View File

@ -175,6 +175,7 @@ model Shifts {
date DateTime @db.Date date DateTime @db.Date
start_time DateTime @db.Time(0) start_time DateTime @db.Time(0)
end_time DateTime @db.Time(0) end_time DateTime @db.Time(0)
is_approved Boolean @default(false)
archive ShiftsArchive[] @relation("ShiftsToArchive") archive ShiftsArchive[] @relation("ShiftsToArchive")
@ -283,6 +284,7 @@ enum LeaveTypes {
BEREAVEMENT // deuil de famille BEREAVEMENT // deuil de famille
PARENTAL // maternite/paternite/adoption PARENTAL // maternite/paternite/adoption
LEGAL // obligations legales comme devoir de juree LEGAL // obligations legales comme devoir de juree
WEDDING // mariage
@@map("leave_types") @@map("leave_types")
} }

View File

@ -18,8 +18,8 @@ import { ArchivalModule } from './modules/archival/archival.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
import { ShiftsValidationModule } from './modules/shifts/validation/shifts-validation.module';
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
import { CsvExportModule } from './modules/exports/csv-exports.module';
@Module({ @Module({
imports: [ imports: [
@ -28,6 +28,7 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod
AuthenticationModule, AuthenticationModule,
BankCodesModule, BankCodesModule,
BusinessLogicsModule, BusinessLogicsModule,
CsvExportModule,
CustomersModule, CustomersModule,
EmployeesModule, EmployeesModule,
ExpensesModule, ExpensesModule,
@ -37,7 +38,6 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod
PayperiodsModule, PayperiodsModule,
PrismaModule, PrismaModule,
ShiftsModule, ShiftsModule,
ShiftsValidationModule,
TimesheetsModule, TimesheetsModule,
UsersModule, UsersModule,
], ],

View File

@ -0,0 +1,26 @@
import { NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
//abstract class for approving or rejecting a shift, expense, timesheet or pay-period
export abstract class BaseApprovalService<T> {
protected constructor(protected readonly prisma: PrismaService) {}
//returns the corresponding Prisma delegate
protected abstract get delegate(): {
update(args: {where: {id: number };
data: { is_approved: boolean }
}): Promise<T>;
};
//standard update Aproval
async updateApproval(id: number, isApproved: boolean): Promise<T> {
const entity = await this.delegate.update({
where: { id },
data: { is_approved: isApproved },
});
if(!entity) throw new NotFoundException(`Entity #${id} not found`);
return entity;
}
}

View File

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

View File

@ -0,0 +1,50 @@
//lenght of a shift, rouded to nearest 'x' minute
export function computeHours(start: Date, end: Date, roundToMinutes?: number): number {
const diffMs = end.getTime() - start.getTime();
const totalMinutes = diffMs / 60000;
const minutes = roundToMinutes ?
Math.round(totalMinutes / roundToMinutes) * roundToMinutes :
totalMinutes;
return +(minutes / 60).toFixed(2);
}
//round the amount of hours to quarter
export function roundToQuarterHour(hours: number): number {
return Math.round(hours *4) / 4;
}
//calculate the number of the week (1 or 2)
export function computeWeekNumber(periodStart: Date, targetDate: Date): number {
const days = Math.floor( targetDate.getTime() - periodStart.getTime()) /
(1000 * 60 * 60 * 24);
return Math.floor(days / 7) +1;
}
//Date format YYY-MM-DD
export function formatDateISO(d:Date): string {
return d.toISOString().split('T')[0];
}
//fetch firts day of the week (Sunday)
export function getWeekStart(date:Date, firstDayOfWeek = 0): Date {
const d = new Date(date);
const day = d.getDay();
const diff = (day < firstDayOfWeek ? 7 : 0) + (day - firstDayOfWeek);
d.setDate(d.getDate() - diff);
d.setHours(0,0,0,0);
return d;
}
//fetch last day of the week (Saturday)
export function getWeekEnd(startOfWeek: Date): Date {
const d = new Date(startOfWeek);
d.setDate(d.getDate() + 6);
d.setHours(23,59,59,999);
return d;
}
//returns january 1st of the selected date's year
export function getYearStart(date:Date): Date {
return new Date(date.getFullYear(),0,1,0,0,0,0);
}

View File

@ -0,0 +1,18 @@
import { PassportSerializer } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class ExpressSessionSerializer extends PassportSerializer {
serializeUser(user: any, done: (err: any, user: any) => void): any {
if (!user){
done(new UnauthorizedException('Serialize user error'), user);
}
done(null, user);
}
deserializeUser(payload: any, done: (err: any, payload: string) => void): any {
if (!payload){
done(new UnauthorizedException('Deserialize user error'), payload);
}
done(null, payload);
}
}

View File

@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "../../../prisma/prisma.service";
//THIS SERVICE IS NOT USED RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING //THIS SERVICE IS NOT USED, RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING
@Injectable() @Injectable()
export class AfterHoursService { export class AfterHoursService {
private readonly logger = new Logger(AfterHoursService.name); private readonly logger = new Logger(AfterHoursService.name);

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "../../../prisma/prisma.service";
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
@Injectable() @Injectable()
export class HolidayService { export class HolidayService {
@ -7,32 +8,15 @@ export class HolidayService {
constructor(private readonly prisma: PrismaService) {} 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> { private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> {
//sets the end of the window to 1ms before the week with the holiday //sets the end of the window to 1ms before the week with the holiday
const holidayWeekStart = this.getWeekStart(holidayDate); const holidayWeekStart = getWeekStart(holidayDate);
const windowEnd = new Date(holidayWeekStart.getTime() - 1); 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 //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 windowStart = new Date(windowEnd.getTime() - 28 * 24 * 60 * 60000 + 1 )
const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700']; const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700'];
//fetches all shift of the employee in said window ( 4 completed weeks ) //fetches all shift of the employee in said window ( 4 previous completed weeks )
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId } , where: { timesheet: { employee_id: employeeId } ,
date: { gte: windowStart, lte: windowEnd }, date: { gte: windowStart, lte: windowEnd },
@ -41,16 +25,16 @@ export class HolidayService {
select: { date: true, start_time: true, end_time: true }, 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 totalHours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
const dailyHours = totalHours / 20; const dailyHours = totalHours / 20;
return dailyHours; return dailyHours;
} }
async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> { async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> {
const hours = await this. computeHoursPrevious4Weeks(employeeId, holidayDate); const hours = await this.computeHoursPrevious4Weeks(employeeId, holidayDate);
const dailyRate = Math.min(hours, 8); const dailyRate = Math.min(hours, 8);
this.logger.debug(`Holiday pay calculation: hours=${hours.toFixed(2)}`); this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
return dailyRate * modifier; return dailyRate * modifier;
} }
} }

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service'; import { PrismaService } from '../../../prisma/prisma.service';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
@Injectable() @Injectable()
export class OvertimeService { export class OvertimeService {
@ -10,47 +11,18 @@ export class OvertimeService {
constructor(private prisma: PrismaService) {} 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 //calculate Daily overtime
getDailyOvertimeHours(start: Date, end: Date): number { getDailyOvertimeHours(start: Date, end: Date): number {
const hours = this.computedHours(start, end); const hours = computeHours(start, end, 5);
const overtime = Math.max(0, hours - this.dailyMax); const overtime = Math.max(0, hours - this.dailyMax);
this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`); this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`);
return overtime; 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 //calculate Weekly overtime
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> { async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
const weekStart = this.getWeekStart(refDate); const weekStart = getWeekStart(refDate);
const weekEnd = this.getWeekEnd(weekStart); const weekEnd = getWeekEnd(weekStart);
//fetches all shifts containing hours //fetches all shifts containing hours
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
@ -63,7 +35,7 @@ export class OvertimeService {
}); });
//calculate total hours of those shifts minus weekly Max to find total overtime hours //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)) const total = shifts.map(shift => computeHours(shift.start_time, shift.end_time, 5))
.reduce((sum, hours)=> sum+hours, 0); .reduce((sum, hours)=> sum+hours, 0);
const overtime = Math.max(0, total - this.weeklyMax); const overtime = Math.max(0, total - this.weeklyMax);

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "../../../prisma/prisma.service";
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
@Injectable() @Injectable()
export class SickLeaveService { export class SickLeaveService {
@ -7,10 +8,10 @@ export class SickLeaveService {
private readonly logger = new Logger(SickLeaveService.name); private readonly logger = new Logger(SickLeaveService.name);
async calculateSickLeavePay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> { async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise<number> {
//sets the year to jan 1st to dec 31st //sets the year to jan 1st to dec 31st
const periodStart = new Date(startDate.getFullYear(), 0, 1); const periodStart = getYearStart(referenceDate);
const periodEnd = startDate; const periodEnd = referenceDate;
//fetches all shifts of a selected employee //fetches all shifts of a selected employee
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
@ -54,7 +55,7 @@ export class SickLeaveService {
const payableDays = Math.min(acquiredDays, daysRequested); const payableDays = Math.min(acquiredDays, daysRequested);
const rawHours = payableDays * 8 * modifier; const rawHours = payableDays * 8 * modifier;
const rounded = Math.round(rawHours * 4) / 4; const rounded = roundToQuarterHour(rawHours)
this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`); this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`);
return rounded; return rounded;
} }

View File

@ -1,19 +1,24 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
import { ExpensesService } from "../services/expenses.service"; import { ExpensesService } from "../services/expenses.service";
import { CreateExpenseDto } from "../dtos/create-expense"; import { CreateExpenseDto } from "../dtos/create-expense.dto";
import { Expenses } from "@prisma/client"; import { Expenses } from "@prisma/client";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { UpdateExpenseDto } from "../dtos/update-expense"; import { UpdateExpenseDto } from "../dtos/update-expense.dto";
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { ExpenseEntity } from "../dtos/swagger-entities/expenses.entity"; import { ExpenseEntity } from "../dtos/swagger-entities/expenses.entity";
import { ExpensesApprovalService } from "../services/expenses-approval.service";
import { SearchExpensesDto } from "../dtos/search-expense.dto";
@ApiTags('Expenses') @ApiTags('Expenses')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
// @UseGuards() // @UseGuards()
@Controller('Expenses') @Controller('Expenses')
export class ExpensesController { export class ExpensesController {
constructor(private readonly expensesService: ExpensesService) {} constructor(
private readonly expensesService: ExpensesService,
private readonly expensesApprovalService: ExpensesApprovalService,
) {}
@Post() @Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ -29,8 +34,9 @@ export class ExpensesController {
@ApiOperation({ summary: 'Find all expenses' }) @ApiOperation({ summary: 'Find all expenses' })
@ApiResponse({ status: 201, description: 'List of expenses found',type: ExpenseEntity, isArray: true }) @ApiResponse({ status: 201, description: 'List of expenses found',type: ExpenseEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of expenses not found' }) @ApiResponse({ status: 400, description: 'List of expenses not found' })
findAll(): Promise<Expenses[]> { @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
return this.expensesService.findAll(); findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
return this.expensesService.findAll(filters);
} }
@Get(':id') @Get(':id')
@ -60,4 +66,10 @@ export class ExpensesController {
return this.expensesService.remove(id); return this.expensesService.remove(id);
} }
@Patch(':id/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
return this.expensesApprovalService.updateApproval(id, isApproved);
}
} }

View File

@ -0,0 +1,26 @@
import { Type } from "class-transformer";
import { IsDateString, IsInt, IsOptional, IsString } from "class-validator";
export class SearchExpensesDto {
@IsOptional()
@Type(()=> Number)
@IsInt()
timesheet_id?: number;
@IsOptional()
@Type(()=> Number)
@IsInt()
bank_code_id?: number;
@IsOptional()
@IsString()
description_contains?: string;
@IsOptional()
@IsDateString()
start_date: string;
@IsOptional()
@IsDateString()
end_date: string;
}

View File

@ -1,4 +1,4 @@
import { PartialType } from "@nestjs/swagger"; import { PartialType } from "@nestjs/swagger";
import { CreateExpenseDto } from "./create-expense"; import { CreateExpenseDto } from "./create-expense.dto";
export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {} export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {}

View File

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

View File

@ -0,0 +1,13 @@
import { Injectable } from "@nestjs/common";
import { Expenses } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class ExpensesApprovalService extends BaseApprovalService<Expenses> {
constructor(prisma: PrismaService) { super(prisma); }
protected get delegate() {
return this.prisma.expenses;
}
}

View File

@ -1,9 +1,11 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { CreateExpenseDto } from "../dtos/create-expense"; import { CreateExpenseDto } from "../dtos/create-expense.dto";
import { Expenses, ExpensesArchive } from "@prisma/client"; import { Expenses, ExpensesArchive } from "@prisma/client";
import { UpdateExpenseDto } from "../dtos/update-expense"; import { UpdateExpenseDto } from "../dtos/update-expense.dto";
import { MileageService } from "src/modules/business-logics/services/mileage.service"; import { MileageService } from "src/modules/business-logics/services/mileage.service";
import { SearchExpensesDto } from "../dtos/search-expense.dto";
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
@Injectable() @Injectable()
export class ExpensesService { export class ExpensesService {
@ -42,10 +44,10 @@ export class ExpensesService {
}) })
} }
findAll(): Promise<Expenses[]> { async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
return this.prisma.expenses.findMany({ const where = buildPrismaWhere(filters);
include: { timesheet: { include: { employee: { include: { user: true } } } } }, const expenses = await this.prisma.expenses.findMany({ where })
}); return expenses;
} }
async findOne(id: number): Promise<Expenses> { async findOne(id: number): Promise<Expenses> {

View File

@ -1,12 +1,13 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseEnumPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
import { LeaveRequestsService } from "../services/leave-requests.service"; import { LeaveRequestsService } from "../services/leave-requests.service";
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
import { LeaveRequests } from "@prisma/client"; import { LeaveRequests } from "@prisma/client";
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client'; import { LeaveApprovalStatus, Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { LeaveRequestEntity } from "../dtos/swagger-entities/leave-requests.entity"; import { LeaveRequestEntity } from "../dtos/swagger-entities/leave-requests.entity";
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
@ApiTags('Leave Requests') @ApiTags('Leave Requests')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@ -29,8 +30,9 @@ export class LeaveRequestController {
@ApiOperation({summary: 'Find all leave request' }) @ApiOperation({summary: 'Find all leave request' })
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestEntity, isArray: true }) @ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of leave request not found' }) @ApiResponse({ status: 400, description: 'List of leave request not found' })
findAll(): Promise<LeaveRequests[]> { @UsePipes(new ValidationPipe({transform: true, whitelist: true}))
return this.leaveRequetsService.findAll(); findAll(@Query() filters: SearchLeaveRequestsDto): Promise<(LeaveRequests & {daysRequested:number; cost: number})[]> {
return this.leaveRequetsService.findAll(filters);
} }
@Get(':id') @Get(':id')
@ -59,4 +61,14 @@ export class LeaveRequestController {
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> { remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
return this.leaveRequetsService.remove(id); return this.leaveRequetsService.remove(id);
} }
}
@Patch(':id/approval')
updateApproval( @Param('id', ParseIntPipe) id: number,
@Body('is_approved', ParseBoolPipe) isApproved: boolean): Promise<LeaveRequests> {
const approvalStatus = isApproved ?
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
}
}

View File

@ -0,0 +1,27 @@
import { LeaveApprovalStatus } from "@prisma/client";
import { Type } from "class-transformer";
import { IsOptional, IsInt, IsEnum, IsDateString } from "class-validator";
export class SearchLeaveRequestsDto {
@IsOptional()
@Type(()=> Number)
@IsInt()
employee_id?: number;
@IsOptional()
@Type(()=> Number)
@IsInt()
bank_code_id?: number;
@IsOptional()
@IsEnum(LeaveApprovalStatus)
approval_status?: LeaveApprovalStatus
@IsOptional()
@IsDateString()
start_date?: Date;
@IsOptional()
@IsDateString()
end_date?: Date;
}

View File

@ -6,6 +6,8 @@ import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
import { HolidayService } from "src/modules/business-logics/services/holiday.service"; import { HolidayService } from "src/modules/business-logics/services/holiday.service";
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
import { VacationService } from "src/modules/business-logics/services/vacation.service"; import { VacationService } from "src/modules/business-logics/services/vacation.service";
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
@Injectable() @Injectable()
export class LeaveRequestsService { export class LeaveRequestsService {
@ -30,8 +32,19 @@ export class LeaveRequestsService {
}); });
} }
async findAll(): Promise<any[]> { async findAll(filters: SearchLeaveRequestsDto): Promise<any[]> {
const {start_date, end_date, ...otherFilters } = filters;
const where: Record<string, any> = buildPrismaWhere(otherFilters);
if (start_date) {
where.start_date_time = { ...(where.start_date_time ?? {}), gte: new Date(start_date) };
}
if(end_date) {
where.end_date_time = { ...(where.end_date_time ?? {}), lte: new Date(end_date) };
}
const list = await this.prisma.leaveRequests.findMany({ const list = await this.prisma.leaveRequests.findMany({
where,
include: { employee: { include: { user: true } }, include: { employee: { include: { user: true } },
bank_code: true, bank_code: true,
}, },

View File

@ -3,12 +3,14 @@ import { PayPeriodsService } from "./services/pay-periods.service";
import { PayPeriodsController } from "./controllers/pay-periods.controller"; import { PayPeriodsController } from "./controllers/pay-periods.controller";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { PayPeriodsOverviewService } from "./services/pay-periods-overview.service"; import { PayPeriodsOverviewService } from "./services/pay-periods-overview.service";
import { PayPeriodsApprovalService } from "./services/pay-periods-approval.service";
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
providers: [ providers: [
PayPeriodsService, PayPeriodsService,
PayPeriodsOverviewService, PayPeriodsOverviewService,
PayPeriodsApprovalService,
], ],
controllers: [PayPeriodsController], controllers: [PayPeriodsController],
}) })

View File

@ -0,0 +1,40 @@
import { NotFoundException } from "@nestjs/common";
import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
export class PayPeriodsApprovalService {
constructor(
private readonly prisma: PrismaService,
private readonly timesheetsApproval: TimesheetsApprovalService,
) {}
async approvaPayperdiod(periodNumber: number): Promise<void> {
const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodNumber },
});
if (!period) throw new NotFoundException(`PayPeriod #${periodNumber} not found`);
//fetches timesheet of selected period if the timesheet as atleast 1 shift or 1 expense
const timesheetList = await this.prisma.timesheets.findMany({
where: {
OR: [
{ shift: {some: { date: { gte: period.start_date,
lte: period.end_date,
},
}},
},
{ expense: { some: { date: { gte: period.start_date,
lte: period.end_date,
},
}},
},
],
},
});
//approval of both timesheet (cascading to the approval of related shifts and expenses)
for(const timesheet of timesheetList) {
await this.timesheetsApproval.updateApproval(timesheet.id, true);
}
}
}

View File

@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto"; import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { computeHours } from "src/common/utils/date-utils";
@Injectable() @Injectable()
export class PayPeriodsOverviewService { export class PayPeriodsOverviewService {
@ -40,7 +41,7 @@ export class PayPeriodsOverviewService {
const user = employee_record.user; const user = employee_record.user;
const employee_id = employee_record.user_id; const employee_id = employee_record.user_id;
const employee_name = `${user.first_name} ${user.last_name}`; const employee_name = `${user.first_name} ${user.last_name}`;
const hours = (shift.end_time.getTime() - shift.start_time.getTime() / 3600000); const hours = computeHours(shift.start_time, shift.end_time);
//check if employee had prior shifts and adds hours of found shift to the total hours //check if employee had prior shifts and adds hours of found shift to the total hours
if (map.has(employee_id)) { if (map.has(employee_id)) {

View File

@ -1,10 +1,15 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException, Param, ParseIntPipe, Patch } from "@nestjs/common";
import { PayPeriods } from "@prisma/client"; import { PayPeriods } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { PayPeriodsApprovalService } from "./pay-periods-approval.service";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Injectable() @Injectable()
export class PayPeriodsService { export class PayPeriodsService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService,
private readonly payperiodsApprovalService: PayPeriodsApprovalService
) {}
async findAll(): Promise<PayPeriods[]> { async findAll(): Promise<PayPeriods[]> {
return this.prisma.payPeriods.findMany({ return this.prisma.payPeriods.findMany({
@ -32,4 +37,13 @@ export class PayPeriodsService {
} }
return period; return period;
} }
@Patch(':periodNumber/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<{message:string}> {
await this.payperiodsApprovalService.approvaPayperdiod(periodNumber);
return {message: `Pay-period #${periodNumber} approved`};
}
} }

View File

@ -1,21 +1,21 @@
import { Controller, Get, Header, Query } from "@nestjs/common"; import { Controller, Get, Header, Query } from "@nestjs/common";
import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service"; import { OverviewRow, ShiftsOverviewService } from "../services/shifts-overview.service";
import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto"; import { GetShiftsOverviewDto } from "../dtos/get-shifts-overview.dto";
@Controller() @Controller()
export class ShiftsValidationController { export class ShiftsOverviewController {
constructor(private readonly shiftsValidationService: ShiftsValidationService) {} constructor(private readonly shiftsValidationService: ShiftsOverviewService) {}
@Get() @Get()
async getSummary( @Query() query: GetShiftsValidationDto): Promise<ValidationRow[]> { async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
return this.shiftsValidationService.getSummary(query.periodId); return this.shiftsValidationService.getSummary(query.period_id);
} }
@Get('export.csv') @Get('export.csv')
@Header('Content-Type', 'text/csv; charset=utf-8') @Header('Content-Type', 'text/csv; charset=utf-8')
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
async exportCsv(@Query() query: GetShiftsValidationDto): Promise<Buffer>{ async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
const rows = await this.shiftsValidationService.getSummary(query.periodId); const rows = await this.shiftsValidationService.getSummary(query.period_id);
//CSV Headers //CSV Headers
const header = [ const header = [

View File

@ -1,19 +1,24 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
import { ShiftsService } from "../services/shifts.service"; import { ShiftsService } from "../services/shifts.service";
import { Shifts } from "@prisma/client"; import { Shifts } from "@prisma/client";
import { CreateShiftDto } from "../dtos/create-shifts.dto"; import { CreateShiftDto } from "../dtos/create-shift.dto";
import { UpdateShiftsDto } from "../dtos/update-shifts.dto"; import { UpdateShiftsDto } from "../dtos/update-shift.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { ShiftEntity } from "../dtos/swagger-entities/shift.entity"; import { ShiftEntity } from "../dtos/swagger-entities/shift.entity";
import { ShiftsApprovalService } from "../services/shifts-approval.service";
import { SearchShiftsDto } from "../dtos/search-shifts.dto";
@ApiTags('Shifts') @ApiTags('Shifts')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
// @UseGuards() // @UseGuards()
@Controller('shifts') @Controller('shifts')
export class ShiftsController { export class ShiftsController {
constructor(private readonly shiftsService: ShiftsService){} constructor(
private readonly shiftsService: ShiftsService,
private readonly shiftsApprovalService: ShiftsApprovalService,
){}
@Post() @Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ -29,8 +34,9 @@ export class ShiftsController {
@ApiOperation({ summary: 'Find all shifts' }) @ApiOperation({ summary: 'Find all shifts' })
@ApiResponse({ status: 201, description: 'List of shifts found',type: ShiftEntity, isArray: true }) @ApiResponse({ status: 201, description: 'List of shifts found',type: ShiftEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of shifts not found' }) @ApiResponse({ status: 400, description: 'List of shifts not found' })
findAll(): Promise<Shifts[]> { @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
return this.shiftsService.findAll(); findAll(@Query() filters: SearchShiftsDto) {
return this.shiftsService.findAll(filters);
} }
@Get(':id') @Get(':id')
@ -60,4 +66,10 @@ export class ShiftsController {
return this.shiftsService.remove(id); return this.shiftsService.remove(id);
} }
@Patch(':id/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
return this.shiftsApprovalService.updateApproval(id, isApproved);
}
} }

View File

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

View File

@ -0,0 +1,29 @@
import { Type } from "class-transformer";
import { IsDateString, IsInt, IsOptional, IsString } from "class-validator";
export class SearchShiftsDto {
@IsOptional()
@Type(()=> Number)
@IsInt()
employee_id?: number;
@IsOptional()
@Type(()=> Number)
@IsInt()
bank_code_id?: number;
@IsOptional()
@IsString()
description_contains?: string;
@IsOptional()
@IsDateString()
end_date?: string;
@IsOptional()
@Type(()=> Number)
@IsInt()
timesheet_id?: number;
}

View File

@ -1,4 +1,4 @@
import { PartialType } from "@nestjs/swagger"; import { PartialType } from "@nestjs/swagger";
import { CreateShiftDto } from "./create-shifts.dto"; import { CreateShiftDto } from "./create-shift.dto";
export class UpdateShiftsDto extends PartialType(CreateShiftDto){} export class UpdateShiftsDto extends PartialType(CreateShiftDto){}

View File

@ -0,0 +1,13 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Shifts } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class ShiftsApprovalService extends BaseApprovalService<Shifts> {
constructor(prisma: PrismaService) { super(prisma); }
protected get delegate() {
return this.prisma.shifts;
}
}

View File

@ -1,7 +1,8 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { computeHours } from "src/common/utils/date-utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
export interface ValidationRow { export interface OverviewRow {
fullName: string; fullName: string;
supervisor: string; supervisor: string;
totalRegularHrs: number; totalRegularHrs: number;
@ -13,22 +14,16 @@ export interface ValidationRow {
} }
@Injectable() @Injectable()
export class ShiftsValidationService { export class ShiftsOverviewService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
private computeHours(start: Date, end: Date): number { async getSummary(period_id: number): Promise<OverviewRow[]> {
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 //fetch pay-period to display
const period = await this.prisma.payPeriods.findUnique({ const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodId }, where: { period_number: period_id },
}); });
if(!period) { if(!period) {
throw new NotFoundException(`pay-period ${periodId} not found`); throw new NotFoundException(`pay-period ${period_id} not found`);
} }
const { start_date, end_date } = period; const { start_date, end_date } = period;
@ -57,7 +52,7 @@ export class ShiftsValidationService {
}, },
}); });
const mapRow = new Map<string, ValidationRow>(); const mapRow = new Map<string, OverviewRow>();
for(const s of shifts) { for(const s of shifts) {
const employeeId = s.timesheet.employee.user_id; const employeeId = s.timesheet.employee.user_id;
@ -77,7 +72,7 @@ export class ShiftsValidationService {
isValidated: false, isValidated: false,
}; };
} }
const hours = this.computeHours(s.start_time, s.end_time); const hours = computeHours(s.start_time, s.end_time);
switch(s.bank_code.type) { switch(s.bank_code.type) {
case 'regular' : row.totalRegularHrs += hours; case 'regular' : row.totalRegularHrs += hours;
@ -119,4 +114,5 @@ export class ShiftsValidationService {
//return by default the list of employee in ascending alphabetical order //return by default the list of employee in ascending alphabetical order
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
} }
} }

View File

@ -1,8 +1,10 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { CreateShiftDto } from "../dtos/create-shifts.dto"; import { CreateShiftDto } from "../dtos/create-shift.dto";
import { Shifts, ShiftsArchive } from "@prisma/client"; import { Shifts, ShiftsArchive } from "@prisma/client";
import { UpdateShiftsDto } from "../dtos/update-shifts.dto"; import { UpdateShiftsDto } from "../dtos/update-shift.dto";
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
import { SearchShiftsDto } from "../dtos/search-shifts.dto";
@Injectable() @Injectable()
export class ShiftsService { export class ShiftsService {
@ -18,10 +20,10 @@ export class ShiftsService {
}); });
} }
findAll(): Promise<Shifts[]> { async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
return this.prisma.shifts.findMany({ const where = buildPrismaWhere(filters);
include: { timesheet: { include: { employee: { include: { user:true } } } } }, const shifts = await this.prisma.shifts.findMany({ where })
}); return shifts;
} }
async findOne(id: number): Promise<Shifts> { async findOne(id: number): Promise<Shifts> {

View File

@ -2,15 +2,14 @@ import { Module } from '@nestjs/common';
import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsController } from './controllers/shifts.controller';
import { ShiftsService } from './services/shifts.service'; import { ShiftsService } from './services/shifts.service';
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
import { ShiftsValidationModule } from './validation/shifts-validation.module'; import { ShiftsOverviewController } from './controllers/shifts-overview.controller';
import { ShiftsOverviewService } from './services/shifts-overview.service';
import { ShiftsApprovalService } from './services/shifts-approval.service';
@Module({ @Module({
imports: [ imports: [BusinessLogicsModule],
BusinessLogicsModule, controllers: [ShiftsController, ShiftsOverviewController],
ShiftsValidationModule, providers: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
], exports: [ShiftsService, ShiftsOverviewService],
controllers: [ShiftsController],
providers: [ShiftsService],
exports: [ShiftsService],
}) })
export class ShiftsModule {} export class ShiftsModule {}

View File

@ -1,11 +0,0 @@
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,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { TimesheetsService } from '../services/timesheets.service'; import { TimesheetsService } from '../services/timesheets.service';
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets } from '@prisma/client'; import { Timesheets } from '@prisma/client';
@ -7,13 +7,18 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { TimesheetEntity } from '../dtos/swagger-entities/timesheet.entity'; import { TimesheetEntity } from '../dtos/swagger-entities/timesheet.entity';
import { TimesheetsApprovalService } from '../services/timesheets-approval.service';
import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
@ApiTags('Timesheets') @ApiTags('Timesheets')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
// @UseGuards() // @UseGuards()
@Controller('timesheets') @Controller('timesheets')
export class TimesheetsController { export class TimesheetsController {
constructor(private readonly timesheetsService: TimesheetsService) {} constructor(
private readonly timesheetsService: TimesheetsService,
private readonly timesheetsApprovalService: TimesheetsApprovalService,
) {}
@Post() @Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ -29,8 +34,9 @@ export class TimesheetsController {
@ApiOperation({ summary: 'Find all timesheets' }) @ApiOperation({ summary: 'Find all timesheets' })
@ApiResponse({ status: 201, description: 'List of timesheet found', type: TimesheetEntity, isArray: true }) @ApiResponse({ status: 201, description: 'List of timesheet found', type: TimesheetEntity, isArray: true })
@ApiResponse({ status: 400, description: 'List of timesheets not found' }) @ApiResponse({ status: 400, description: 'List of timesheets not found' })
findAll(): Promise<Timesheets[]> { @UsePipes(new ValidationPipe({transform: true, whitelist: true }))
return this.timesheetsService.findAll(); findAll(@Query() filters: SearchTimesheetDto): Promise<any[]> {
return this.timesheetsService.findAll(filters);
} }
@Get(':id') @Get(':id')
@ -62,4 +68,10 @@ export class TimesheetsController {
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> { remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
return this.timesheetsService.remove(id); return this.timesheetsService.remove(id);
} }
@Patch(':id/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
return this.timesheetsApprovalService.updateApproval(id, isApproved);
}
} }

View File

@ -0,0 +1,20 @@
import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional } from "class-validator";
export class SearchTimesheetDto {
@IsOptional()
@Type(() => Number)
@IsInt()
timesheet_id: number;
@IsOptional()
@Type(()=> Number)
@IsInt()
employee_id: number;
@IsOptional()
@Type(()=> Boolean)
@IsBoolean()
is_approved: boolean;
}

View File

@ -0,0 +1,37 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Timesheets } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { ExpensesApprovalService } from "src/modules/expenses/services/expenses-approval.service";
import { ShiftsApprovalService } from "src/modules/shifts/services/shifts-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class TimesheetsApprovalService extends BaseApprovalService<Timesheets>{
constructor(
prisma: PrismaService,
private readonly shiftsApproval: ShiftsApprovalService,
private readonly expensesApproval: ExpensesApprovalService,
) {super(prisma);}
protected get delegate() {
return this.prisma.timesheets;
}
async updateApproval(timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await super.updateApproval(timesheetId, isApproved);
await this.prisma.shifts.updateMany({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
await this.prisma.expenses.updateMany({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
return timesheet;
}
}

View File

@ -4,6 +4,9 @@ import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { Timesheets, TimesheetsArchive } from '@prisma/client';
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
import { computeHours } from 'src/common/utils/date-utils';
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
@Injectable() @Injectable()
export class TimesheetsService { export class TimesheetsService {
@ -23,32 +26,38 @@ export class TimesheetsService {
}); });
} }
async findAll(): Promise<any[]> { async findAll(filters: SearchTimesheetDto): Promise<any[]> {
const list = await this.prisma.timesheets.findMany({ const where = buildPrismaWhere(filters);
include: {
shift: { include: { bank_code: true } }, //fetchs lists of shifts and expenses for a selected timesheet
const rawlist = await this.prisma.timesheets.findMany({
where, include: {
shift: { include: {bank_code: true } },
expense: { include: { bank_code: true } }, expense: { include: { bank_code: true } },
employee: { include: { user : true } }, employee: { include: { user : true } },
}, },
}); });
return Promise.all( const detailedlist = await Promise.all(
list.map(async timesheet => { rawlist.map(async timesheet => {
const detailedShifts = timesheet.shift.map(s => { //detailed shifts
const hours = this.overtime.computedHours(s.start_time, s.end_time); const detailedShifts = timesheet.shift.map(shift => {
const regularHours = Math.min(8, hours); const totalhours = computeHours(shift.start_time, shift.end_time,5);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); const regularHours = Math.min(8, totalhours);
const payRegular = regularHours * s.bank_code.modifier; const dailyOvertime = this.overtime.getDailyOvertimeHours(shift.start_time, shift.end_time);
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); const payRegular = regularHours * shift.bank_code.modifier;
return { ...s, hours, payRegular, payOvertime }; const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, shift.bank_code.modifier);
return { ...shift, totalhours, payRegular, payOvertime };
}); });
//calculate overtime weekly
const weeklyOvertimeHours = detailedShifts.length const weeklyOvertimeHours = detailedShifts.length
? await this.overtime.getWeeklyOvertimeHours( ? await this.overtime.getWeeklyOvertimeHours(
timesheet.employee_id, timesheet.employee_id,
timesheet.shift[0].date): 0; timesheet.shift[0].date): 0;
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
}) }),
); );
return detailedlist;
} }
async findOne(id: number): Promise<any> { async findOne(id: number): Promise<any> {
@ -65,7 +74,7 @@ export class TimesheetsService {
} }
const detailedShifts = timesheet.shift.map( s => { const detailedShifts = timesheet.shift.map( s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time); const hours = computeHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours); const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier; const payRegular = regularHours * s.bank_code.modifier;

View File

@ -2,11 +2,19 @@ import { Module } from '@nestjs/common';
import { TimesheetsController } from './controllers/timesheets.controller'; import { TimesheetsController } from './controllers/timesheets.controller';
import { TimesheetsService } from './services/timesheets.service'; import { TimesheetsService } from './services/timesheets.service';
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
import { TimesheetsApprovalService } from './services/timesheets-approval.service';
import { ShiftsApprovalService } from '../shifts/services/shifts-approval.service';
import { ExpensesApprovalService } from '../expenses/services/expenses-approval.service';
@Module({ @Module({
imports: [BusinessLogicsModule], imports: [BusinessLogicsModule],
controllers: [TimesheetsController], controllers: [TimesheetsController],
providers: [ TimesheetsService ], providers: [
TimesheetsService,
TimesheetsApprovalService,
ShiftsApprovalService,
ExpensesApprovalService
],
exports: [TimesheetsService], exports: [TimesheetsService],
}) })
export class TimesheetsModule {} export class TimesheetsModule {}