fix(merge): conflicts resolved
This commit is contained in:
commit
f4c69c4620
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
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";
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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,7 +25,7 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
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 { 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) {}
|
||||||
|
|
@ -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 ],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
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 { 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> {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 { 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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 { 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)) {
|
||||||
|
|
|
||||||
|
|
@ -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`};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 = [
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
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 { 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){}
|
||||||
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 { 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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: {
|
|
||||||
|
//fetchs lists of shifts and expenses for a selected timesheet
|
||||||
|
const rawlist = await this.prisma.timesheets.findMany({
|
||||||
|
where, include: {
|
||||||
shift: { include: {bank_code: true } },
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user