Merge branch 'main' of git.targo.ca:Targo/targo_backend into dev/setup/attachment/MatthieuH
This commit is contained in:
commit
514f41df19
|
|
@ -2,6 +2,7 @@ import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client";
|
import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
|
import { LeaveRequestViewDto } from "src/modules/leave-requests/dtos/leave-request.view.dto";
|
||||||
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
||||||
|
|
||||||
@ApiTags('LeaveRequests Archives')
|
@ApiTags('LeaveRequests Archives')
|
||||||
|
|
@ -22,7 +23,7 @@ export class LeaveRequestsArchiveController {
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'})
|
@ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'})
|
||||||
@ApiResponse({ status: 200, description: 'Archived leaveRequest found'})
|
@ApiResponse({ status: 200, description: 'Archived leaveRequest found'})
|
||||||
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestsArchive> {
|
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestViewDto> {
|
||||||
try{
|
try{
|
||||||
return await this.leaveRequestsService.findOneArchived(id);
|
return await this.leaveRequestsService.findOneArchived(id);
|
||||||
}catch {
|
}catch {
|
||||||
|
|
|
||||||
|
|
@ -18,54 +18,67 @@ export interface CsvRow {
|
||||||
export class CsvExportService {
|
export class CsvExportService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async collectTransaction(period_id: number, companies: ExportCompany[]): Promise<CsvRow[]> {
|
async collectTransaction( period_id: number, companies: ExportCompany[], approved: boolean = true):
|
||||||
const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
Promise<CsvRow[]> {
|
||||||
|
|
||||||
|
const company_codes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||||
|
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
where: { pay_period_no: period_id },
|
where: { pay_period_no: period_id },
|
||||||
});
|
});
|
||||||
if(!period) {
|
if(!period) throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||||
throw new NotFoundException(`Pay period ${period_id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDate = period.period_start;
|
const start_date = period.period_start;
|
||||||
const endDate = period.period_end;
|
const end_date = period.period_end;
|
||||||
|
|
||||||
|
const approved_filter = approved ? { is_approved: true } : {};
|
||||||
|
|
||||||
//fetching shifts
|
//fetching shifts
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: { date: { gte: startDate, lte: endDate },
|
where: {
|
||||||
timesheet: { employee: { company_code: { in: companyCodes} } },
|
date: { gte: start_date, lte: end_date },
|
||||||
|
...approved_filter,
|
||||||
|
timesheet: {
|
||||||
|
employee: { company_code: { in: company_codes} } },
|
||||||
},
|
},
|
||||||
include: { bank_code: true,
|
include: {
|
||||||
timesheet: { include: {employee: { include: { user:true,
|
bank_code: true,
|
||||||
supervisor: { include: { user:true } },
|
timesheet: { include: {
|
||||||
}},
|
employee: { include: {
|
||||||
}},
|
user:true,
|
||||||
|
supervisor: { include: {
|
||||||
|
user:true } } } } } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//fetching expenses
|
//fetching expenses
|
||||||
const expenses = await this.prisma.expenses.findMany({
|
const expenses = await this.prisma.expenses.findMany({
|
||||||
where: { date: { gte: startDate, lte: endDate },
|
where: {
|
||||||
timesheet: { employee: { company_code: { in: companyCodes} } },
|
date: { gte: start_date, lte: end_date },
|
||||||
|
...approved_filter,
|
||||||
|
timesheet: { employee: { company_code: { in: company_codes} } },
|
||||||
},
|
},
|
||||||
include: { bank_code: true,
|
include: { bank_code: true,
|
||||||
timesheet: { include: { employee: { include: { user: true,
|
timesheet: { include: {
|
||||||
supervisor: { include: { user:true } },
|
employee: { include: {
|
||||||
} },
|
user: true,
|
||||||
} },
|
supervisor: { include: {
|
||||||
},
|
user:true } } } } } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//fetching leave requests
|
//fetching leave requests
|
||||||
const leaves = await this.prisma.leaveRequests.findMany({
|
const leaves = await this.prisma.leaveRequests.findMany({
|
||||||
where : { start_date_time: { gte: startDate, lte: endDate },
|
where : {
|
||||||
employee: { company_code: { in: companyCodes } },
|
start_date_time: { gte: start_date, lte: end_date },
|
||||||
|
employee: { company_code: { in: company_codes } },
|
||||||
},
|
},
|
||||||
include: { bank_code: true,
|
include: {
|
||||||
employee: { include: { user: true,
|
bank_code: true,
|
||||||
supervisor: { include: { user: true } },
|
employee: { include: {
|
||||||
}},
|
user: true,
|
||||||
|
supervisor: { include: {
|
||||||
|
user: true } } } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -74,7 +87,7 @@ export class CsvExportService {
|
||||||
//Shifts Mapping
|
//Shifts Mapping
|
||||||
for (const shift of shifts) {
|
for (const shift of shifts) {
|
||||||
const emp = shift.timesheet.employee;
|
const emp = shift.timesheet.employee;
|
||||||
const week_number = this.computeWeekNumber(startDate, shift.date);
|
const week_number = this.computeWeekNumber(start_date, shift.date);
|
||||||
const hours = this.computeHours(shift.start_time, shift.end_time);
|
const hours = this.computeHours(shift.start_time, shift.end_time);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
|
|
@ -85,7 +98,7 @@ export class CsvExportService {
|
||||||
quantity_hours: hours,
|
quantity_hours: hours,
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
week_number,
|
week_number,
|
||||||
pay_date: this.formatDate(endDate),
|
pay_date: this.formatDate(end_date),
|
||||||
holiday_date: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +106,7 @@ export class CsvExportService {
|
||||||
//Expenses Mapping
|
//Expenses Mapping
|
||||||
for (const e of expenses) {
|
for (const e of expenses) {
|
||||||
const emp = e.timesheet.employee;
|
const emp = e.timesheet.employee;
|
||||||
const week_number = this.computeWeekNumber(startDate, e.date);
|
const week_number = this.computeWeekNumber(start_date, e.date);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
company_code: emp.company_code,
|
company_code: emp.company_code,
|
||||||
|
|
@ -103,7 +116,7 @@ export class CsvExportService {
|
||||||
quantity_hours: undefined,
|
quantity_hours: undefined,
|
||||||
amount: Number(e.amount),
|
amount: Number(e.amount),
|
||||||
week_number,
|
week_number,
|
||||||
pay_date: this.formatDate(endDate),
|
pay_date: this.formatDate(end_date),
|
||||||
holiday_date: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +128,7 @@ export class CsvExportService {
|
||||||
const start = l.start_date_time;
|
const start = l.start_date_time;
|
||||||
const end = l.end_date_time ?? start;
|
const end = l.end_date_time ?? start;
|
||||||
|
|
||||||
const week_number = this.computeWeekNumber(startDate, start);
|
const week_number = this.computeWeekNumber(start_date, start);
|
||||||
const hours = this.computeHours(start, end);
|
const hours = this.computeHours(start, end);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
|
|
@ -126,7 +139,7 @@ export class CsvExportService {
|
||||||
quantity_hours: hours,
|
quantity_hours: hours,
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
week_number,
|
week_number,
|
||||||
pay_date: this.formatDate(endDate),
|
pay_date: this.formatDate(end_date),
|
||||||
holiday_date: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +159,7 @@ export class CsvExportService {
|
||||||
generateCsv(rows: CsvRow[]): Buffer {
|
generateCsv(rows: CsvRow[]): Buffer {
|
||||||
const header = [
|
const header = [
|
||||||
'company_code',
|
'company_code',
|
||||||
'external_payrol_id',
|
'external_payroll_id',
|
||||||
'full_name',
|
'full_name',
|
||||||
'bank_code',
|
'bank_code',
|
||||||
'quantity_hours',
|
'quantity_hours',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseEnumPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, 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-request.dto";
|
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||||
import { LeaveRequests } from "@prisma/client";
|
import { LeaveRequests } from "@prisma/client";
|
||||||
|
|
@ -7,6 +7,7 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { LeaveApprovalStatus, 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 { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||||
|
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||||
|
|
||||||
@ApiTags('Leave Requests')
|
@ApiTags('Leave Requests')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -20,35 +21,35 @@ export class LeaveRequestController {
|
||||||
@ApiOperation({summary: 'Create leave request' })
|
@ApiOperation({summary: 'Create leave request' })
|
||||||
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
||||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||||
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
|
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||||
return this. leaveRequetsService.create(dto);
|
return this. leaveRequetsService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({summary: 'Find all leave request' })
|
@ApiOperation({summary: 'Find all leave request' })
|
||||||
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: CreateLeaveRequestsDto, isArray: true })
|
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestViewDto, isArray: true })
|
||||||
@ApiResponse({ status: 400, description: 'List of leave request not found' })
|
@ApiResponse({ status: 400, description: 'List of leave request not found' })
|
||||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true}))
|
@UsePipes(new ValidationPipe({transform: true, whitelist: true}))
|
||||||
findAll(@Query() filters: SearchLeaveRequestsDto): Promise<(LeaveRequests & {daysRequested:number; cost: number})[]> {
|
findAll(@Query() filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||||
return this.leaveRequetsService.findAll(filters);
|
return this.leaveRequetsService.findAll(filters);
|
||||||
}
|
}
|
||||||
//remove emp_id and use email
|
//remove emp_id and use email
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({summary: 'Find leave request' })
|
@ApiOperation({summary: 'Find leave request' })
|
||||||
@ApiResponse({ status: 201, description: 'Leave request found',type: CreateLeaveRequestsDto })
|
@ApiResponse({ status: 201, description: 'Leave request found',type: LeaveRequestViewDto })
|
||||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
|
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||||
return this.leaveRequetsService.findOne(id);
|
return this.leaveRequetsService.findOne(id);
|
||||||
}
|
}
|
||||||
//remove emp_id and use email
|
//remove emp_id and use email
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({summary: 'Update leave request' })
|
@ApiOperation({summary: 'Update leave request' })
|
||||||
@ApiResponse({ status: 201, description: 'Leave request updated',type: CreateLeaveRequestsDto })
|
@ApiResponse({ status: 201, description: 'Leave request updated',type: LeaveRequestViewDto })
|
||||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||||
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateLeaveRequestsDto): Promise<LeaveRequests> {
|
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||||
return this.leaveRequetsService.update(id, dto);
|
return this.leaveRequetsService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,14 +59,14 @@ export class LeaveRequestController {
|
||||||
@ApiOperation({summary: 'Delete leave request' })
|
@ApiOperation({summary: 'Delete leave request' })
|
||||||
@ApiResponse({ status: 201, description: 'Leave request deleted',type: CreateLeaveRequestsDto })
|
@ApiResponse({ status: 201, description: 'Leave request deleted',type: CreateLeaveRequestsDto })
|
||||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
|
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||||
return this.leaveRequetsService.remove(id);
|
return this.leaveRequetsService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//remove emp_id and use email
|
//remove emp_id and use email
|
||||||
@Patch(':id/approval')
|
@Patch(':id/approval')
|
||||||
updateApproval( @Param('id', ParseIntPipe) id: number,
|
updateApproval( @Param('id', ParseIntPipe) id: number,
|
||||||
@Body('is_approved', ParseBoolPipe) is_approved: boolean): Promise<LeaveRequests> {
|
@Body('is_approved', ParseBoolPipe) is_approved: boolean): Promise<LeaveRequestViewDto> {
|
||||||
const approvalStatus = is_approved ?
|
const approvalStatus = is_approved ?
|
||||||
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
|
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
|
||||||
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
|
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,12 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
import { IsDateString, IsEmail, IsEnum, IsInt, IsISO8601, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class CreateLeaveRequestsDto {
|
export class CreateLeaveRequestsDto {
|
||||||
@ApiProperty({
|
|
||||||
example: 1,
|
|
||||||
description: 'Leave request`s unique id(auto-incremented)',
|
|
||||||
})
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
//remove emp_id and use email
|
@IsEmail()
|
||||||
@ApiProperty({
|
email: string;
|
||||||
example: '4655867',
|
|
||||||
description: 'Employee`s id',
|
|
||||||
})
|
|
||||||
@Type(()=> Number)
|
|
||||||
@IsInt()
|
|
||||||
employee_id: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 7,
|
example: 7,
|
||||||
|
|
@ -38,17 +27,16 @@ export class CreateLeaveRequestsDto {
|
||||||
example: '22/06/2463',
|
example: '22/06/2463',
|
||||||
description: 'Leave request`s start date',
|
description: 'Leave request`s start date',
|
||||||
})
|
})
|
||||||
@Type(()=>Date)
|
@IsISO8601()
|
||||||
@IsNotEmpty()
|
start_date_time:string;
|
||||||
start_date_time:Date;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '25/03/3019',
|
example: '25/03/3019',
|
||||||
description: 'Leave request`s end date',
|
description: 'Leave request`s end date',
|
||||||
})
|
})
|
||||||
@Type(()=>Date)
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
end_date_time?: Date;
|
@IsISO8601()
|
||||||
|
end_date_time?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'My precious',
|
example: 'My precious',
|
||||||
|
|
|
||||||
13
src/modules/leave-requests/dtos/leave-request.view.dto.ts
Normal file
13
src/modules/leave-requests/dtos/leave-request.view.dto.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||||
|
|
||||||
|
export class LeaveRequestViewDto {
|
||||||
|
id!: number;
|
||||||
|
leave_type!: LeaveTypes;
|
||||||
|
start_date_time!: string;
|
||||||
|
end_date_time!: string | null;
|
||||||
|
comment!: string | null;
|
||||||
|
approval_status: LeaveApprovalStatus;
|
||||||
|
email!: string;
|
||||||
|
employee_full_name: string;
|
||||||
|
days_requested?: number;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { LeaveApprovalStatus } from "@prisma/client";
|
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsOptional, IsInt, IsEnum, IsDateString } from "class-validator";
|
import { IsOptional, IsInt, IsEnum, IsDateString, IsEmail } from "class-validator";
|
||||||
|
|
||||||
export class SearchLeaveRequestsDto {
|
export class SearchLeaveRequestsDto {
|
||||||
//remove emp_id and use email
|
|
||||||
@IsOptional()
|
@IsEmail()
|
||||||
@Type(()=> Number)
|
email: string;
|
||||||
@IsInt()
|
|
||||||
employee_id?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(()=> Number)
|
@Type(()=> Number)
|
||||||
|
|
@ -20,9 +18,13 @@ export class SearchLeaveRequestsDto {
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
start_date?: Date;
|
start_date?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
end_date?: Date;
|
end_date?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(LeaveTypes)
|
||||||
|
leave_type?: LeaveTypes;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||||
|
import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select";
|
||||||
|
|
||||||
|
const toISO = (date: Date | null): string | null => (date ? date.toISOString().slice(0,10): null);
|
||||||
|
|
||||||
|
export function mapArchiveRowToView(row: LeaveRequestArchiveRow, email: string, employee_full_name:string): LeaveRequestViewDto {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
leave_type: row.leave_type,
|
||||||
|
start_date_time: toISO(row.start_date_time)!,
|
||||||
|
end_date_time: toISO(row.end_date_time),
|
||||||
|
comment: row.comment,
|
||||||
|
approval_status: row.approval_status,
|
||||||
|
email,
|
||||||
|
employee_full_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/modules/leave-requests/mappers/leave-requests.mapper.ts
Normal file
19
src/modules/leave-requests/mappers/leave-requests.mapper.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||||
|
import { LeaveRequestRow } from "../utils/leave-requests.select";
|
||||||
|
|
||||||
|
function toISODateString(date:Date | null): string | null {
|
||||||
|
return date ? date.toISOString().slice(0,10) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapRowToView(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
leave_type: row.leave_type,
|
||||||
|
start_date_time: toISODateString(row.start_date_time)!,
|
||||||
|
end_date_time: toISODateString(row.end_date_time),
|
||||||
|
comment: row.comment,
|
||||||
|
approval_status: row.approval_status,
|
||||||
|
email: row.employee.user.email,
|
||||||
|
employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,13 @@ import { SickLeaveService } from "src/modules/business-logics/services/sick-leav
|
||||||
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-request.dto";
|
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||||
|
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||||
|
import { LeaveRequestRow, leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||||
|
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||||
|
import { LeaveRequestsArchiveController } from "src/modules/archival/controllers/leave-requests-archive.controller";
|
||||||
|
import { LeaveRequestArchiveRow, leaveRequestsArchiveSelect } from "../utils/leave-requests-archive.select";
|
||||||
|
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
||||||
|
import { mapArchiveRowToViewWithDays, mapRowToViewWithDays } from "../utils/leave-request.transform";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LeaveRequestsService {
|
export class LeaveRequestsService {
|
||||||
|
|
@ -18,138 +25,93 @@ export class LeaveRequestsService {
|
||||||
private readonly sickLeaveService: SickLeaveService
|
private readonly sickLeaveService: SickLeaveService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//remove emp_id and use email
|
//function to avoid using employee_id as identifier in the frontend.
|
||||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
|
private async resolveEmployeeIdByEmail(email: string): Promise<number> {
|
||||||
const { employee_id, bank_code_id, leave_type, start_date_time,
|
const employee = await this.prisma.employees.findFirst({
|
||||||
end_date_time, comment, approval_status } = dto;
|
where: { user: { email} },
|
||||||
|
select: { id:true },
|
||||||
return this.prisma.leaveRequests.create({
|
|
||||||
data: { employee_id, bank_code_id, leave_type, start_date_time,
|
|
||||||
end_date_time, comment, approval_status: approval_status ?? undefined
|
|
||||||
},
|
|
||||||
include: { employee: { include: { user: true } },
|
|
||||||
bank_code: true
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
if(!employee) throw new NotFoundException(`Employee with email ${email} not found`);
|
||||||
|
return employee.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(filters: SearchLeaveRequestsDto): Promise<any[]> {
|
//create a leave-request without the use of employee_id
|
||||||
const {start_date, end_date, ...other_filters } = filters;
|
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||||
const where: Record<string, any> = buildPrismaWhere(other_filters);
|
const employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||||
|
const row: LeaveRequestRow = await this.prisma.leaveRequests.create({
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ms_per_day = 1000 * 60 * 60 * 24;
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
list.map(async request => {
|
|
||||||
// end_date fallback
|
|
||||||
const end_date = request.end_date_time ?? request.start_date_time;
|
|
||||||
|
|
||||||
//Requested days
|
|
||||||
const diff_days = Math.round((end_date.getTime() - request.start_date_time.getTime()) / ms_per_day) +1;
|
|
||||||
|
|
||||||
// modifier fallback/validation
|
|
||||||
if (!request.bank_code || request.bank_code.modifier == null) {
|
|
||||||
throw new BadRequestException(`Modifier manquant pour bank_code_id=${request.bank_code_id}`);
|
|
||||||
}
|
|
||||||
const modifier = request.bank_code.modifier;
|
|
||||||
|
|
||||||
let cost: number;
|
|
||||||
switch (request.bank_code.type) {
|
|
||||||
case 'holiday' :
|
|
||||||
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
|
|
||||||
break;
|
|
||||||
case 'vacation' :
|
|
||||||
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diff_days, modifier );
|
|
||||||
break;
|
|
||||||
case 'sick' :
|
|
||||||
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diff_days, modifier );
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cost = diff_days * modifier;
|
|
||||||
}
|
|
||||||
return {...request, days_requested: diff_days, cost };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//remove emp_id and use email
|
|
||||||
async findOne(id:number): Promise<any> {
|
|
||||||
const request = await this.prisma.leaveRequests.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: { employee: { include: { user: true } },
|
|
||||||
bank_code: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if(!request) {
|
|
||||||
throw new NotFoundException(`LeaveRequest #${id} not found`);
|
|
||||||
}
|
|
||||||
//validation and fallback for end_date_time
|
|
||||||
const end_Date = request.end_date_time ?? request.start_date_time;
|
|
||||||
|
|
||||||
//calculate included days
|
|
||||||
const msPerDay = 1000 * 60 * 60 * 24;
|
|
||||||
const diff_days = Math.floor((end_Date.getTime() - request.start_date_time.getTime())/ msPerDay) + 1;
|
|
||||||
|
|
||||||
if (!request.bank_code || request.bank_code.modifier == null) {
|
|
||||||
throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`);
|
|
||||||
}
|
|
||||||
const modifier = request.bank_code.modifier;
|
|
||||||
|
|
||||||
//calculate cost based on bank_code types
|
|
||||||
let cost = diff_days * modifier;
|
|
||||||
switch(request.bank_code.type) {
|
|
||||||
case 'holiday':
|
|
||||||
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
|
|
||||||
break;
|
|
||||||
case 'vacation':
|
|
||||||
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diff_days, modifier );
|
|
||||||
break;
|
|
||||||
case 'sick':
|
|
||||||
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diff_days, modifier );
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cost = diff_days * modifier;
|
|
||||||
}
|
|
||||||
return {...request, days_requested: diff_days, cost };
|
|
||||||
}
|
|
||||||
|
|
||||||
//remove emp_id and use email
|
|
||||||
async update(id: number, dto: UpdateLeaveRequestsDto): Promise<LeaveRequests> {
|
|
||||||
await this.findOne(id);
|
|
||||||
const { employee_id, leave_type, start_date_time, end_date_time, comment, approval_status } = dto;
|
|
||||||
return this.prisma.leaveRequests.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
data: {
|
||||||
...(employee_id !== undefined && { employee_id }),
|
employee_id,
|
||||||
...(leave_type !== undefined && { leave_type } ),
|
bank_code_id: dto.bank_code_id,
|
||||||
...(start_date_time !== undefined && { start_date_time }),
|
leave_type: dto.leave_type,
|
||||||
...(end_date_time !== undefined && { end_date_time }),
|
start_date_time: new Date(dto.start_date_time),
|
||||||
...(comment !== undefined && { comment }),
|
end_date_time: dto.end_date_time ? new Date(dto.end_date_time) : null,
|
||||||
...(approval_status !== undefined && { approval_status }),
|
comment: dto.comment,
|
||||||
|
approval_status: dto.approval_status ?? undefined,
|
||||||
},
|
},
|
||||||
include: { employee: { include: { user:true } } },
|
select: leaveRequestsSelect,
|
||||||
});
|
});
|
||||||
|
return mapRowToViewWithDays(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id:number): Promise<LeaveRequests> {
|
//fetches all leave-requests using email
|
||||||
await this.findOne(id);
|
async findAll(filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||||
return this.prisma.leaveRequests.delete({
|
const {start_date, end_date,email, leave_type, approval_status, bank_code_id } = filters;
|
||||||
where: { id },
|
const where: any = {};
|
||||||
|
|
||||||
|
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) };
|
||||||
|
if (email) where.employee = { user: { email } };
|
||||||
|
if (leave_type) where.leave_type = leave_type;
|
||||||
|
if (approval_status) where.approval_status = approval_status;
|
||||||
|
if (typeof bank_code_id === 'number') where.bank_code_id = bank_code_id;
|
||||||
|
|
||||||
|
const rows= await this.prisma.leaveRequests.findMany({
|
||||||
|
where,
|
||||||
|
select: leaveRequestsSelect,
|
||||||
|
orderBy: { start_date_time: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return rows.map(mapRowToViewWithDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
//fetch 1 leave-request using email
|
||||||
|
async findOne(id:number): Promise<LeaveRequestViewDto> {
|
||||||
|
const row: LeaveRequestRow | null = await this.prisma.leaveRequests.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: leaveRequestsSelect,
|
||||||
|
});
|
||||||
|
if(!row) throw new NotFoundException(`Leave Request #${id} not found`);
|
||||||
|
return mapRowToViewWithDays(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
//updates 1 leave-request using email
|
||||||
|
async update(id: number, dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||||
|
await this.findOne(id);
|
||||||
|
const data: Record<string, any> = {};
|
||||||
|
|
||||||
|
if(dto.email !== undefined) data.employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||||
|
if(dto.leave_type !== undefined) data.bank_code_id = dto.bank_code_id;
|
||||||
|
if(dto.start_date_time !== undefined) data.start_date_time = new Date(dto.start_date_time);
|
||||||
|
if(dto.end_date_time !== undefined) data.end_date_time = new Date(dto.end_date_time);
|
||||||
|
if(dto.comment !== undefined) data.comment = dto.comment;
|
||||||
|
if(dto.approval_status !== undefined) data.approval_status = dto.approval_status;
|
||||||
|
|
||||||
|
const row: LeaveRequestRow = await this.prisma.leaveRequests.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
select: leaveRequestsSelect,
|
||||||
|
});
|
||||||
|
return mapRowToViewWithDays(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
//removes 1 leave-request using email
|
||||||
|
async remove(id:number): Promise<LeaveRequestViewDto> {
|
||||||
|
await this.findOne(id);
|
||||||
|
const row: LeaveRequestRow = await this.prisma.leaveRequests.delete({
|
||||||
|
where: { id },
|
||||||
|
select: leaveRequestsSelect,
|
||||||
|
});
|
||||||
|
return mapRowToViewWithDays(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
//archivation functions ******************************************************
|
//archivation functions ******************************************************
|
||||||
|
|
@ -184,14 +146,30 @@ export class LeaveRequestsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//fetches all archived employees
|
//fetches all archived leave-requests
|
||||||
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
||||||
return this.prisma.leaveRequestsArchive.findMany();
|
return this.prisma.leaveRequestsArchive.findMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
//remove emp_id and use email
|
//remove emp_id and use email
|
||||||
//fetches an archived employee
|
//fetches an archived employee
|
||||||
async findOneArchived(id: number): Promise<LeaveRequestsArchive> {
|
async findOneArchived(id: number): Promise<LeaveRequestViewDto> {
|
||||||
return this.prisma.leaveRequestsArchive.findUniqueOrThrow({ where: { id } });
|
const row: LeaveRequestArchiveRow | null = await this.prisma.leaveRequestsArchive.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: leaveRequestsArchiveSelect,
|
||||||
|
});
|
||||||
|
if(!row) throw new NotFoundException(`Archived Leave Request #${id} not found`);
|
||||||
|
|
||||||
|
const emp = await this.prisma.employees.findUnique({
|
||||||
|
where: { id: row.employee_id },
|
||||||
|
select: { user: {select: { email:true,
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
}}},
|
||||||
|
});
|
||||||
|
const email = emp?.user.email ?? "";
|
||||||
|
const full_name = emp ? `${emp.user.first_name} ${emp.user.last_name}` : "";
|
||||||
|
|
||||||
|
return mapArchiveRowToViewWithDays(row, email, full_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
src/modules/leave-requests/utils/leave-request.transform.ts
Normal file
32
src/modules/leave-requests/utils/leave-request.transform.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||||
|
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
||||||
|
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||||
|
import { LeaveRequestArchiveRow } from "./leave-requests-archive.select";
|
||||||
|
import { LeaveRequestRow } from "./leave-requests.select";
|
||||||
|
|
||||||
|
function toUTCDateOnly(date: Date): Date {
|
||||||
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const MS_PER_DAY = 86_400_000;
|
||||||
|
function computeDaysRequested(start_date: Date, end_date?: Date | null): number {
|
||||||
|
const start = toUTCDateOnly(start_date);
|
||||||
|
const end = toUTCDateOnly(end_date ?? start_date);
|
||||||
|
const diff = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
|
||||||
|
return Math.max(1, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Active (table leave_requests) : map + days_requested */
|
||||||
|
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||||
|
const view = mapRowToView(row);
|
||||||
|
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Archive (table leave_requests_archive) : map + days_requested */
|
||||||
|
export function mapArchiveRowToViewWithDays(row: LeaveRequestArchiveRow, email: string, employee_full_name?: string):
|
||||||
|
LeaveRequestViewDto {
|
||||||
|
const view = mapArchiveRowToView(row, email, employee_full_name!);
|
||||||
|
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
export const leaveRequestsArchiveSelect = {
|
||||||
|
id: true,
|
||||||
|
leave_request_id: true,
|
||||||
|
archived_at: true,
|
||||||
|
employee_id: true,
|
||||||
|
leave_type: true,
|
||||||
|
start_date_time: true,
|
||||||
|
end_date_time: true,
|
||||||
|
comment: true,
|
||||||
|
approval_status: true,
|
||||||
|
|
||||||
|
} satisfies Prisma.LeaveRequestsArchiveSelect;
|
||||||
|
|
||||||
|
export type LeaveRequestArchiveRow = Prisma.LeaveRequestsArchiveGetPayload<{ select: typeof leaveRequestsArchiveSelect}>;
|
||||||
22
src/modules/leave-requests/utils/leave-requests.select.ts
Normal file
22
src/modules/leave-requests/utils/leave-requests.select.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
//custom prisma select to avoid employee_id exposure
|
||||||
|
export const leaveRequestsSelect = {
|
||||||
|
id: true,
|
||||||
|
bank_code_id: true,
|
||||||
|
leave_type: true,
|
||||||
|
start_date_time: true,
|
||||||
|
end_date_time: true,
|
||||||
|
comment: true,
|
||||||
|
approval_status: true,
|
||||||
|
employee: { select: {
|
||||||
|
id: true,
|
||||||
|
user: { select: {
|
||||||
|
email: true,
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
} satisfies Prisma.LeaveRequestsSelect;
|
||||||
|
|
||||||
|
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
||||||
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
import { TimesheetsQueryService } from '../services/timesheets-query.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';
|
||||||
|
|
@ -8,6 +8,7 @@ import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
||||||
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||||
|
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
|
|
||||||
@ApiTags('Timesheets')
|
@ApiTags('Timesheets')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -15,8 +16,8 @@ import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||||
@Controller('timesheets')
|
@Controller('timesheets')
|
||||||
export class TimesheetsController {
|
export class TimesheetsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timesheetsService: TimesheetsQueryService,
|
private readonly timesheetsQuery: TimesheetsQueryService,
|
||||||
private readonly timesheetsCommandService: TimesheetsCommandService,
|
private readonly timesheetsCommand: TimesheetsCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|
@ -25,17 +26,18 @@ export class TimesheetsController {
|
||||||
@ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto })
|
@ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto })
|
||||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||||
create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
|
create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
|
||||||
return this.timesheetsService.create(dto);
|
return this.timesheetsQuery.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Find all timesheets' })
|
async getPeriodByQuery(
|
||||||
@ApiResponse({ status: 201, description: 'List of timesheet found', type: CreateTimesheetDto, isArray: true })
|
@Query('year', ParseIntPipe ) year: number,
|
||||||
@ApiResponse({ status: 400, description: 'List of timesheets not found' })
|
@Query('period_no', ParseIntPipe ) period_no: number,
|
||||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true }))
|
@Query('email') email?: string
|
||||||
findAll(@Query() filters: SearchTimesheetDto): Promise<any[]> {
|
): Promise<TimesheetPeriodDto> {
|
||||||
return this.timesheetsService.findAll(filters);
|
if(!email || !email.trim()) throw new BadRequestException('Query param "email" is mandatory for this route.');
|
||||||
|
return this.timesheetsQuery.findAll(year, period_no, email.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|
@ -44,7 +46,7 @@ export class TimesheetsController {
|
||||||
@ApiResponse({ status: 201, description: 'Timesheet found', type: CreateTimesheetDto })
|
@ApiResponse({ status: 201, description: 'Timesheet found', type: CreateTimesheetDto })
|
||||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||||
return this.timesheetsService.findOne(id);
|
return this.timesheetsQuery.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
|
|
@ -56,7 +58,7 @@ export class TimesheetsController {
|
||||||
@Param('id', ParseIntPipe) id:number,
|
@Param('id', ParseIntPipe) id:number,
|
||||||
@Body() dto: UpdateTimesheetDto,
|
@Body() dto: UpdateTimesheetDto,
|
||||||
): Promise<Timesheets> {
|
): Promise<Timesheets> {
|
||||||
return this.timesheetsService.update(id, dto);
|
return this.timesheetsQuery.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
|
@ -65,12 +67,12 @@ export class TimesheetsController {
|
||||||
@ApiResponse({ status: 201, description: 'Timesheet deleted', type: CreateTimesheetDto })
|
@ApiResponse({ status: 201, description: 'Timesheet deleted', type: CreateTimesheetDto })
|
||||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||||
return this.timesheetsService.remove(id);
|
return this.timesheetsQuery.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/approval')
|
@Patch(':id/approval')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
||||||
return this.timesheetsCommandService.updateApproval(id, isApproved);
|
return this.timesheetsCommand.updateApproval(id, isApproved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/modules/timesheets/dtos/timesheet-period.dto.ts
Normal file
45
src/modules/timesheets/dtos/timesheet-period.dto.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
export class ShiftDto {
|
||||||
|
start: string;
|
||||||
|
end : string;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpenseDto {
|
||||||
|
amount: number;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DayShiftsDto = ShiftDto[];
|
||||||
|
|
||||||
|
export class DayExpensesDto {
|
||||||
|
cash: ExpenseDto[] = [];
|
||||||
|
km : ExpenseDto[] = [];
|
||||||
|
[otherType:string]: ExpenseDto[] | any; //pour si on ajoute d'autre type de dépenses
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeekDto {
|
||||||
|
is_approved: boolean;
|
||||||
|
shifts: {
|
||||||
|
sun: DayShiftsDto;
|
||||||
|
mon: DayShiftsDto;
|
||||||
|
tue: DayShiftsDto;
|
||||||
|
wed: DayShiftsDto;
|
||||||
|
thu: DayShiftsDto;
|
||||||
|
fri: DayShiftsDto;
|
||||||
|
sat: DayShiftsDto;
|
||||||
|
}
|
||||||
|
expenses: {
|
||||||
|
sun: DayExpensesDto;
|
||||||
|
mon: DayExpensesDto;
|
||||||
|
tue: DayExpensesDto;
|
||||||
|
wed: DayExpensesDto;
|
||||||
|
thu: DayExpensesDto;
|
||||||
|
fri: DayExpensesDto;
|
||||||
|
sat: DayExpensesDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimesheetPeriodDto {
|
||||||
|
week1: WeekDto;
|
||||||
|
week2: WeekDto;
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,12 @@ import { OvertimeService } from 'src/modules/business-logics/services/overtime.s
|
||||||
import { computeHours } from 'src/common/utils/date-utils';
|
import { computeHours } from 'src/common/utils/date-utils';
|
||||||
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
||||||
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||||
|
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
|
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||||
|
|
||||||
const ROUND_TO = 5;
|
// deprecated (used with old findAll) const ROUND_TO = 5;
|
||||||
|
type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean };
|
||||||
|
type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean };
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsQueryService {
|
export class TimesheetsQueryService {
|
||||||
|
|
@ -30,38 +34,63 @@ export class TimesheetsQueryService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(filters: SearchTimesheetDto): Promise<any[]> {
|
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
||||||
const where = buildPrismaWhere(filters);
|
//finds the employee
|
||||||
|
const employee = await this.prisma.employees.findFirst({
|
||||||
//fetchs lists of shifts and expenses for a selected timesheet
|
where: { user: { is: { email } } },
|
||||||
const rawlist = await this.prisma.timesheets.findMany({
|
select: { id: true },
|
||||||
where, include: {
|
|
||||||
shift: { include: {bank_code: true } },
|
|
||||||
expense: { include: { bank_code: true } },
|
|
||||||
employee: { include: { user : true } },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
if(!employee) throw new NotFoundException(`no employee with email ${email} found`);
|
||||||
|
|
||||||
const detailedlist = await Promise.all(
|
//finds the period
|
||||||
rawlist.map(async timesheet => {
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
//detailed shifts
|
where: { pay_year: year, pay_period_no: period_no },
|
||||||
const detailedShifts = timesheet.shift.map(shift => {
|
select: { period_start: true, period_end: true },
|
||||||
const totalhours = computeHours(shift.start_time, shift.end_time, ROUND_TO);
|
});
|
||||||
const regularHours = Math.min(8, totalhours);
|
if(!period) throw new NotFoundException(`Period ${year}-${period_no} not found`);
|
||||||
const dailyOvertime = this.overtime.getDailyOvertimeHours(shift.start_time, shift.end_time);
|
|
||||||
const payRegular = regularHours * shift.bank_code.modifier;
|
const from = toUTCDateOnly(period.period_start);
|
||||||
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, shift.bank_code.modifier);
|
const to = endOfDayUTC(period.period_end);
|
||||||
return { ...shift, totalhours, payRegular, payOvertime };
|
|
||||||
});
|
//collects data from shifts and expenses
|
||||||
//calculate overtime weekly
|
const [ raw_shifts, raw_expenses] = await Promise.all([
|
||||||
const weeklyOvertimeHours = detailedShifts.length
|
this.prisma.shifts.findMany({
|
||||||
? await this.overtime.getWeeklyOvertimeHours(
|
where: {
|
||||||
timesheet.employee_id,
|
timesheet: { is: { employee_id: employee.id } },
|
||||||
timesheet.shift[0].date): 0;
|
date: { gte: from, lte: to },
|
||||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
},
|
||||||
|
select: { date: true,start_time: true, end_time: true, is_approved: true },
|
||||||
|
orderBy: [{date: 'asc'}, { start_time: 'asc' }],
|
||||||
}),
|
}),
|
||||||
);
|
this.prisma.expenses.findMany({
|
||||||
return detailedlist;
|
where: {
|
||||||
|
timesheet: { is: { employee_id: employee.id } },
|
||||||
|
date: { gte: from, lte: to },
|
||||||
|
},
|
||||||
|
select: { date: true, amount: true, is_approved: true, bank_code: {
|
||||||
|
select: { type: true } },
|
||||||
|
},
|
||||||
|
orderBy: { date: 'asc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
//Shift data mapping
|
||||||
|
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
||||||
|
date: shift.date,
|
||||||
|
start_time: shift.start_time,
|
||||||
|
end_time: shift.end_time,
|
||||||
|
is_approved: shift.is_approved ?? true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
||||||
|
date: expense.date,
|
||||||
|
amount: typeof (expense.amount as any)?.toNumber() === 'function' ?
|
||||||
|
(expense.amount as any).toNumber() : Number(expense.amount),
|
||||||
|
type: expense.bank_code?.type ?? 'CASH',
|
||||||
|
is_approved: expense.is_approved ?? true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return buildPeriod(period.period_start, period.period_end, shifts , expenses);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number): Promise<any> {
|
async findOne(id: number): Promise<any> {
|
||||||
|
|
|
||||||
134
src/modules/timesheets/utils/timesheet.helpers.ts
Normal file
134
src/modules/timesheets/utils/timesheet.helpers.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { DayExpensesDto, DayShiftsDto, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto";
|
||||||
|
|
||||||
|
//makes the strings indexes for arrays
|
||||||
|
export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const;
|
||||||
|
export type DayKey = 'sun'|'mon'|'tue'|'wed'|'thu'|'fri'|'sat';
|
||||||
|
|
||||||
|
//DB line types
|
||||||
|
type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean };
|
||||||
|
type ExpenseRow = {date: Date; amount: number; type: string; is_approved?: boolean };
|
||||||
|
|
||||||
|
export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
||||||
|
const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday
|
||||||
|
return DAY_KEYS[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MS_PER_DAY = 86_400_000;
|
||||||
|
|
||||||
|
export function toUTCDateOnly(date: Date | string): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDays(date:Date, days: number): Date {
|
||||||
|
return new Date(date.getTime() + days * MS_PER_DAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endOfDayUTC(date: Date | string): Date {
|
||||||
|
const d = toUTCDateOnly(date);
|
||||||
|
return new Date(d.getTime() + MS_PER_DAY - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): boolean {
|
||||||
|
const time = date.getTime();
|
||||||
|
return time >= start.getTime() && time <= end_inclusive.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dayIndex(week_start: Date, date: Date): 1|2|3|4|5|6|7 {
|
||||||
|
const diff = Math.floor((toUTCDateOnly(date).getTime() - toUTCDateOnly(week_start).getTime())/ MS_PER_DAY);
|
||||||
|
const index = Math.min(6, Math.max(0, diff)) + 1;
|
||||||
|
return index as 1|2|3|4|5|6|7;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTimeString(date: Date): string {
|
||||||
|
const hours = String(date.getUTCHours()).padStart(2,'0');
|
||||||
|
const minutes = String(date.getUTCMinutes()).padStart(2,'0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function round2(num: number) {
|
||||||
|
return Math.round(num * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEmptyDayShifts(): DayShiftsDto { return []; }
|
||||||
|
|
||||||
|
export function makeEmptyDayExpenses(): DayExpensesDto { return {cash: [], km: []}; }
|
||||||
|
|
||||||
|
export function makeEmptyWeek(): WeekDto {
|
||||||
|
return {
|
||||||
|
is_approved: true,
|
||||||
|
shifts: {
|
||||||
|
sun: makeEmptyDayShifts(),
|
||||||
|
mon: makeEmptyDayShifts(),
|
||||||
|
tue: makeEmptyDayShifts(),
|
||||||
|
wed: makeEmptyDayShifts(),
|
||||||
|
thu: makeEmptyDayShifts(),
|
||||||
|
fri: makeEmptyDayShifts(),
|
||||||
|
sat: makeEmptyDayShifts(),
|
||||||
|
},
|
||||||
|
expenses: {
|
||||||
|
sun: makeEmptyDayExpenses(),
|
||||||
|
mon: makeEmptyDayExpenses(),
|
||||||
|
tue: makeEmptyDayExpenses(),
|
||||||
|
wed: makeEmptyDayExpenses(),
|
||||||
|
thu: makeEmptyDayExpenses(),
|
||||||
|
fri: makeEmptyDayExpenses(),
|
||||||
|
sat: makeEmptyDayExpenses(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEmptyPeriod(): TimesheetPeriodDto {
|
||||||
|
return { week1: makeEmptyWeek(), week2: makeEmptyWeek() };
|
||||||
|
}
|
||||||
|
|
||||||
|
//needs ajusting according to DB's data for expenses types
|
||||||
|
export function normalizeExpenseBucket(db_type: string): 'km' | 'cash' {
|
||||||
|
const type = db_type.trim().toUpperCase();
|
||||||
|
if(type.includes('KM') || type.includes('MILEAGE')) return 'km';
|
||||||
|
return 'cash';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): WeekDto {
|
||||||
|
const week = makeEmptyWeek();
|
||||||
|
let all_approved = true;
|
||||||
|
|
||||||
|
//Shifts mapped and filtered by dates
|
||||||
|
const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end));
|
||||||
|
for (const shift of week_shifts) {
|
||||||
|
const key = dayKeyFromDate(shift.date, true);
|
||||||
|
week.shifts[key].push({
|
||||||
|
start: toTimeString(shift.start_time),
|
||||||
|
end : toTimeString(shift.end_time),
|
||||||
|
is_approved: shift.is_approved ?? true,
|
||||||
|
} as ShiftDto);
|
||||||
|
all_approved = all_approved && (shift.is_approved ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Expenses mapped and filtered by dates
|
||||||
|
const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end));
|
||||||
|
for (const expense of week_expenses) {
|
||||||
|
const key = dayKeyFromDate(expense.date, true);
|
||||||
|
const bucket = normalizeExpenseBucket(expense.type);
|
||||||
|
if (!Array.isArray(week.expenses[key][bucket])) week.expenses[key][bucket] = [];
|
||||||
|
week.expenses[key][bucket].push({
|
||||||
|
amount: round2(expense.amount),
|
||||||
|
is_approved: expense.is_approved ?? true,
|
||||||
|
});
|
||||||
|
all_approved = all_approved && (expense.is_approved ?? true);
|
||||||
|
}
|
||||||
|
week.is_approved = all_approved;
|
||||||
|
return week;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPeriod( period_start: Date, period_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): TimesheetPeriodDto {
|
||||||
|
const week1_start = toUTCDateOnly(period_start);
|
||||||
|
const week1_end = endOfDayUTC(addDays(week1_start, 6));
|
||||||
|
const week2_start = toUTCDateOnly(addDays(week1_start, 7));
|
||||||
|
const week2_end = endOfDayUTC(period_end);
|
||||||
|
|
||||||
|
return {
|
||||||
|
week1: buildWeek(week1_start, week1_end, shifts, expenses),
|
||||||
|
week2: buildWeek(week2_start, week2_end, shifts, expenses),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user