fix(merge): conflicts resolved
This commit is contained in:
commit
f4c69c4620
|
|
@ -3,7 +3,7 @@
|
|||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"operationId": "ShiftsValidationController_getSummary",
|
||||
"operationId": "ShiftsOverviewController_getSummary",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"get": {
|
||||
"operationId": "ShiftsValidationController_exportCsv",
|
||||
"operationId": "ShiftsOverviewController_exportCsv",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -943,7 +999,7 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"post": {
|
||||
"operationId": "CustomersController_create",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "shifts" ADD COLUMN "is_approved" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
|
@ -175,6 +175,7 @@ model Shifts {
|
|||
date DateTime @db.Date
|
||||
start_time DateTime @db.Time(0)
|
||||
end_time DateTime @db.Time(0)
|
||||
is_approved Boolean @default(false)
|
||||
|
||||
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
||||
|
||||
|
|
@ -283,6 +284,7 @@ enum LeaveTypes {
|
|||
BEREAVEMENT // deuil de famille
|
||||
PARENTAL // maternite/paternite/adoption
|
||||
LEGAL // obligations legales comme devoir de juree
|
||||
WEDDING // mariage
|
||||
|
||||
@@map("leave_types")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ 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';
|
||||
import { ShiftsValidationModule } from './modules/shifts/validation/shifts-validation.module';
|
||||
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
|
||||
import { CsvExportModule } from './modules/exports/csv-exports.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -28,6 +28,7 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod
|
|||
AuthenticationModule,
|
||||
BankCodesModule,
|
||||
BusinessLogicsModule,
|
||||
CsvExportModule,
|
||||
CustomersModule,
|
||||
EmployeesModule,
|
||||
ExpensesModule,
|
||||
|
|
@ -37,7 +38,6 @@ import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.mod
|
|||
PayperiodsModule,
|
||||
PrismaModule,
|
||||
ShiftsModule,
|
||||
ShiftsValidationModule,
|
||||
TimesheetsModule,
|
||||
UsersModule,
|
||||
],
|
||||
|
|
|
|||
26
src/common/shared/base-approval.service.ts
Normal file
26
src/common/shared/base-approval.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
21
src/common/shared/build-prisma-where.ts
Normal file
21
src/common/shared/build-prisma-where.ts
Normal 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;
|
||||
}
|
||||
50
src/common/utils/date-utils.ts
Normal file
50
src/common/utils/date-utils.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ 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
|
||||
//THIS SERVICE IS NOT USED, RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING
|
||||
@Injectable()
|
||||
export class AfterHoursService {
|
||||
private readonly logger = new Logger(AfterHoursService.name);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||
|
||||
@Injectable()
|
||||
export class HolidayService {
|
||||
|
|
@ -7,32 +8,15 @@ export class HolidayService {
|
|||
|
||||
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 holidayWeekStart = 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 )
|
||||
//fetches all shift of the employee in said window ( 4 previous completed weeks )
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: { timesheet: { employee_id: employeeId } ,
|
||||
date: { gte: windowStart, lte: windowEnd },
|
||||
|
|
@ -41,16 +25,16 @@ export class HolidayService {
|
|||
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;
|
||||
|
||||
return dailyHours;
|
||||
}
|
||||
|
||||
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);
|
||||
this.logger.debug(`Holiday pay calculation: hours=${hours.toFixed(2)}`);
|
||||
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
|
||||
return dailyRate * modifier;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
||||
|
||||
@Injectable()
|
||||
export class OvertimeService {
|
||||
|
|
@ -10,47 +11,18 @@ export class OvertimeService {
|
|||
|
||||
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 hours = computeHours(start, end, 5);
|
||||
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);
|
||||
const weekStart = getWeekStart(refDate);
|
||||
const weekEnd = getWeekEnd(weekStart);
|
||||
|
||||
//fetches all shifts containing hours
|
||||
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
|
||||
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);
|
||||
const overtime = Math.max(0, total - this.weeklyMax);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
|
||||
@Injectable()
|
||||
export class SickLeaveService {
|
||||
|
|
@ -7,10 +8,10 @@ export class SickLeaveService {
|
|||
|
||||
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
|
||||
const periodStart = new Date(startDate.getFullYear(), 0, 1);
|
||||
const periodEnd = startDate;
|
||||
const periodStart = getYearStart(referenceDate);
|
||||
const periodEnd = referenceDate;
|
||||
|
||||
//fetches all shifts of a selected employee
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
|
|
@ -54,7 +55,7 @@ export class SickLeaveService {
|
|||
|
||||
const payableDays = Math.min(acquiredDays, daysRequested);
|
||||
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}`);
|
||||
return rounded;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { CreateExpenseDto } from "../dtos/create-expense";
|
||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||
import { Expenses } 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 { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { ExpenseEntity } from "../dtos/swagger-entities/expenses.entity";
|
||||
import { ExpensesApprovalService } from "../services/expenses-approval.service";
|
||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||
|
||||
@ApiTags('Expenses')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('Expenses')
|
||||
export class ExpensesController {
|
||||
constructor(private readonly expensesService: ExpensesService) {}
|
||||
constructor(
|
||||
private readonly expensesService: ExpensesService,
|
||||
private readonly expensesApprovalService: ExpensesApprovalService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
|
|
@ -29,8 +34,9 @@ export class ExpensesController {
|
|||
@ApiOperation({ summary: 'Find all expenses' })
|
||||
@ApiResponse({ status: 201, description: 'List of expenses found',type: ExpenseEntity, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of expenses not found' })
|
||||
findAll(): Promise<Expenses[]> {
|
||||
return this.expensesService.findAll();
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
return this.expensesService.findAll(filters);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
@ -60,4 +66,10 @@ export class ExpensesController {
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
26
src/modules/expenses/dtos/search-expense.dto.ts
Normal file
26
src/modules/expenses/dtos/search-expense.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateExpenseDto } from "./create-expense";
|
||||
import { CreateExpenseDto } from "./create-expense.dto";
|
||||
|
||||
export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {}
|
||||
|
|
@ -1,13 +1,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";
|
||||
import { ExpensesApprovalService } from "./services/expenses-approval.service";
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
controllers: [ExpensesController],
|
||||
providers: [ExpensesService],
|
||||
providers: [ExpensesService, ExpensesApprovalService],
|
||||
exports: [ ExpensesService ],
|
||||
})
|
||||
|
||||
|
|
|
|||
13
src/modules/expenses/services/expenses-approval.service.ts
Normal file
13
src/modules/expenses/services/expenses-approval.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
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 { UpdateExpenseDto } from "../dtos/update-expense";
|
||||
import { UpdateExpenseDto } from "../dtos/update-expense.dto";
|
||||
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()
|
||||
export class ExpensesService {
|
||||
|
|
@ -42,10 +44,10 @@ export class ExpensesService {
|
|||
})
|
||||
}
|
||||
|
||||
findAll(): Promise<Expenses[]> {
|
||||
return this.prisma.expenses.findMany({
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } } },
|
||||
});
|
||||
async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
const expenses = await this.prisma.expenses.findMany({ where })
|
||||
return expenses;
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Expenses> {
|
||||
|
|
|
|||
|
|
@ -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 { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
|
||||
import { LeaveRequests } from "@prisma/client";
|
||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
|
||||
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 { LeaveRequestEntity } from "../dtos/swagger-entities/leave-requests.entity";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -29,8 +30,9 @@ export class LeaveRequestController {
|
|||
@ApiOperation({summary: 'Find all leave request' })
|
||||
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestEntity, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of leave request not found' })
|
||||
findAll(): Promise<LeaveRequests[]> {
|
||||
return this.leaveRequetsService.findAll();
|
||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true}))
|
||||
findAll(@Query() filters: SearchLeaveRequestsDto): Promise<(LeaveRequests & {daysRequested:number; cost: number})[]> {
|
||||
return this.leaveRequetsService.findAll(filters);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
@ -59,4 +61,14 @@ export class LeaveRequestController {
|
|||
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
|
||||
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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
27
src/modules/leave-requests/dtos/search-leave-requests.dto.ts
Normal file
27
src/modules/leave-requests/dtos/search-leave-requests.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ 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";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
|
||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||
|
||||
@Injectable()
|
||||
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({
|
||||
where,
|
||||
include: { employee: { include: { user: true } },
|
||||
bank_code: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { PayPeriodsService } from "./services/pay-periods.service";
|
|||
import { PayPeriodsController } from "./controllers/pay-periods.controller";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PayPeriodsOverviewService } from "./services/pay-periods-overview.service";
|
||||
import { PayPeriodsApprovalService } from "./services/pay-periods-approval.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [
|
||||
PayPeriodsService,
|
||||
PayPeriodsOverviewService,
|
||||
PayPeriodsApprovalService,
|
||||
],
|
||||
controllers: [PayPeriodsController],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
|
|||
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
|
||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { computeHours } from "src/common/utils/date-utils";
|
||||
|
||||
@Injectable()
|
||||
export class PayPeriodsOverviewService {
|
||||
|
|
@ -40,7 +41,7 @@ export class PayPeriodsOverviewService {
|
|||
const user = employee_record.user;
|
||||
const employee_id = employee_record.user_id;
|
||||
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
|
||||
if (map.has(employee_id)) {
|
||||
|
|
|
|||
|
|
@ -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 { 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()
|
||||
export class PayPeriodsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(private readonly prisma: PrismaService,
|
||||
private readonly payperiodsApprovalService: PayPeriodsApprovalService
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<PayPeriods[]> {
|
||||
return this.prisma.payPeriods.findMany({
|
||||
|
|
@ -32,4 +37,13 @@ export class PayPeriodsService {
|
|||
}
|
||||
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`};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
import { Controller, Get, Header, Query } from "@nestjs/common";
|
||||
import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service";
|
||||
import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto";
|
||||
import { OverviewRow, ShiftsOverviewService } from "../services/shifts-overview.service";
|
||||
import { GetShiftsOverviewDto } from "../dtos/get-shifts-overview.dto";
|
||||
|
||||
@Controller()
|
||||
export class ShiftsValidationController {
|
||||
constructor(private readonly shiftsValidationService: ShiftsValidationService) {}
|
||||
export class ShiftsOverviewController {
|
||||
constructor(private readonly shiftsValidationService: ShiftsOverviewService) {}
|
||||
|
||||
@Get()
|
||||
async getSummary( @Query() query: GetShiftsValidationDto): Promise<ValidationRow[]> {
|
||||
return this.shiftsValidationService.getSummary(query.periodId);
|
||||
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
||||
return this.shiftsValidationService.getSummary(query.period_id);
|
||||
}
|
||||
|
||||
@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);
|
||||
async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
|
||||
const rows = await this.shiftsValidationService.getSummary(query.period_id);
|
||||
|
||||
//CSV Headers
|
||||
const header = [
|
||||
|
|
@ -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 { Shifts } from "@prisma/client";
|
||||
import { CreateShiftDto } from "../dtos/create-shifts.dto";
|
||||
import { UpdateShiftsDto } from "../dtos/update-shifts.dto";
|
||||
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
||||
import { UpdateShiftsDto } from "../dtos/update-shift.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 { ShiftEntity } from "../dtos/swagger-entities/shift.entity";
|
||||
import { ShiftsApprovalService } from "../services/shifts-approval.service";
|
||||
import { SearchShiftsDto } from "../dtos/search-shifts.dto";
|
||||
|
||||
@ApiTags('Shifts')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('shifts')
|
||||
export class ShiftsController {
|
||||
constructor(private readonly shiftsService: ShiftsService){}
|
||||
constructor(
|
||||
private readonly shiftsService: ShiftsService,
|
||||
private readonly shiftsApprovalService: ShiftsApprovalService,
|
||||
){}
|
||||
|
||||
@Post()
|
||||
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
|
|
@ -29,8 +34,9 @@ export class ShiftsController {
|
|||
@ApiOperation({ summary: 'Find all shifts' })
|
||||
@ApiResponse({ status: 201, description: 'List of shifts found',type: ShiftEntity, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of shifts not found' })
|
||||
findAll(): Promise<Shifts[]> {
|
||||
return this.shiftsService.findAll();
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchShiftsDto) {
|
||||
return this.shiftsService.findAll(filters);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
@ -60,4 +66,10 @@ export class ShiftsController {
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { Type } from "class-transformer";
|
||||
import { IsInt, Min, Max } from "class-validator";
|
||||
|
||||
export class GetShiftsValidationDto {
|
||||
export class GetShiftsOverviewDto {
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(26)
|
||||
periodId: number;
|
||||
period_id: number;
|
||||
}
|
||||
29
src/modules/shifts/dtos/search-shifts.dto.ts
Normal file
29
src/modules/shifts/dtos/search-shifts.dto.ts
Normal 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;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateShiftDto } from "./create-shifts.dto";
|
||||
import { CreateShiftDto } from "./create-shift.dto";
|
||||
|
||||
export class UpdateShiftsDto extends PartialType(CreateShiftDto){}
|
||||
13
src/modules/shifts/services/shifts-approval.service.ts
Normal file
13
src/modules/shifts/services/shifts-approval.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { computeHours } from "src/common/utils/date-utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
export interface ValidationRow {
|
||||
export interface OverviewRow {
|
||||
fullName: string;
|
||||
supervisor: string;
|
||||
totalRegularHrs: number;
|
||||
|
|
@ -13,22 +14,16 @@ export interface ValidationRow {
|
|||
}
|
||||
|
||||
@Injectable()
|
||||
export class ShiftsValidationService {
|
||||
export class ShiftsOverviewService {
|
||||
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[]> {
|
||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||
//fetch pay-period to display
|
||||
const period = await this.prisma.payPeriods.findUnique({
|
||||
where: { period_number: periodId },
|
||||
where: { period_number: period_id },
|
||||
});
|
||||
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;
|
||||
|
||||
|
|
@ -57,7 +52,7 @@ export class ShiftsValidationService {
|
|||
},
|
||||
});
|
||||
|
||||
const mapRow = new Map<string, ValidationRow>();
|
||||
const mapRow = new Map<string, OverviewRow>();
|
||||
|
||||
for(const s of shifts) {
|
||||
const employeeId = s.timesheet.employee.user_id;
|
||||
|
|
@ -77,7 +72,7 @@ export class ShiftsValidationService {
|
|||
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) {
|
||||
case 'regular' : row.totalRegularHrs += hours;
|
||||
|
|
@ -119,4 +114,5 @@ export class ShiftsValidationService {
|
|||
//return by default the list of employee in ascending alphabetical order
|
||||
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
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 { 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()
|
||||
export class ShiftsService {
|
||||
|
|
@ -18,10 +20,10 @@ export class ShiftsService {
|
|||
});
|
||||
}
|
||||
|
||||
findAll(): Promise<Shifts[]> {
|
||||
return this.prisma.shifts.findMany({
|
||||
include: { timesheet: { include: { employee: { include: { user:true } } } } },
|
||||
});
|
||||
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
const shifts = await this.prisma.shifts.findMany({ where })
|
||||
return shifts;
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Shifts> {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ import { Module } from '@nestjs/common';
|
|||
import { ShiftsController } from './controllers/shifts.controller';
|
||||
import { ShiftsService } from './services/shifts.service';
|
||||
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({
|
||||
imports: [
|
||||
BusinessLogicsModule,
|
||||
ShiftsValidationModule,
|
||||
],
|
||||
controllers: [ShiftsController],
|
||||
providers: [ShiftsService],
|
||||
exports: [ShiftsService],
|
||||
imports: [BusinessLogicsModule],
|
||||
controllers: [ShiftsController, ShiftsOverviewController],
|
||||
providers: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
||||
exports: [ShiftsService, ShiftsOverviewService],
|
||||
})
|
||||
export class ShiftsModule {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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 { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
||||
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 { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { TimesheetEntity } from '../dtos/swagger-entities/timesheet.entity';
|
||||
import { TimesheetsApprovalService } from '../services/timesheets-approval.service';
|
||||
import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
|
||||
|
||||
@ApiTags('Timesheets')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('timesheets')
|
||||
export class TimesheetsController {
|
||||
constructor(private readonly timesheetsService: TimesheetsService) {}
|
||||
constructor(
|
||||
private readonly timesheetsService: TimesheetsService,
|
||||
private readonly timesheetsApprovalService: TimesheetsApprovalService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
|
|
@ -29,8 +34,9 @@ export class TimesheetsController {
|
|||
@ApiOperation({ summary: 'Find all timesheets' })
|
||||
@ApiResponse({ status: 201, description: 'List of timesheet found', type: TimesheetEntity, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of timesheets not found' })
|
||||
findAll(): Promise<Timesheets[]> {
|
||||
return this.timesheetsService.findAll();
|
||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchTimesheetDto): Promise<any[]> {
|
||||
return this.timesheetsService.findAll(filters);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
@ -62,4 +68,10 @@ export class TimesheetsController {
|
|||
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
src/modules/timesheets/dtos/search-timesheets.dto.ts
Normal file
20
src/modules/timesheets/dtos/search-timesheets.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
|||
import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
||||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
||||
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()
|
||||
export class TimesheetsService {
|
||||
|
|
@ -23,32 +26,38 @@ export class TimesheetsService {
|
|||
});
|
||||
}
|
||||
|
||||
async findAll(): Promise<any[]> {
|
||||
const list = await this.prisma.timesheets.findMany({
|
||||
include: {
|
||||
shift: { include: { bank_code: true } },
|
||||
async findAll(filters: SearchTimesheetDto): Promise<any[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
|
||||
//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 } },
|
||||
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 detailedlist = await Promise.all(
|
||||
rawlist.map(async timesheet => {
|
||||
//detailed shifts
|
||||
const detailedShifts = timesheet.shift.map(shift => {
|
||||
const totalhours = computeHours(shift.start_time, shift.end_time,5);
|
||||
const regularHours = Math.min(8, totalhours);
|
||||
const dailyOvertime = this.overtime.getDailyOvertimeHours(shift.start_time, shift.end_time);
|
||||
const payRegular = regularHours * shift.bank_code.modifier;
|
||||
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, shift.bank_code.modifier);
|
||||
return { ...shift, totalhours, payRegular, payOvertime };
|
||||
});
|
||||
//calculate overtime weekly
|
||||
const weeklyOvertimeHours = detailedShifts.length
|
||||
? await this.overtime.getWeeklyOvertimeHours(
|
||||
timesheet.employee_id,
|
||||
timesheet.shift[0].date): 0;
|
||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||
})
|
||||
}),
|
||||
);
|
||||
return detailedlist;
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<any> {
|
||||
|
|
@ -65,7 +74,7 @@ export class TimesheetsService {
|
|||
}
|
||||
|
||||
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 dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
|
||||
const payRegular = regularHours * s.bank_code.modifier;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@ 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';
|
||||
import { TimesheetsApprovalService } from './services/timesheets-approval.service';
|
||||
import { ShiftsApprovalService } from '../shifts/services/shifts-approval.service';
|
||||
import { ExpensesApprovalService } from '../expenses/services/expenses-approval.service';
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
controllers: [TimesheetsController],
|
||||
providers: [ TimesheetsService ],
|
||||
providers: [
|
||||
TimesheetsService,
|
||||
TimesheetsApprovalService,
|
||||
ShiftsApprovalService,
|
||||
ExpensesApprovalService
|
||||
],
|
||||
exports: [TimesheetsService],
|
||||
})
|
||||
export class TimesheetsModule {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user