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 { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client";
|
||||
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";
|
||||
|
||||
@ApiTags('LeaveRequests Archives')
|
||||
|
|
@ -22,7 +23,7 @@ export class LeaveRequestsArchiveController {
|
|||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'})
|
||||
@ApiResponse({ status: 200, description: 'Archived leaveRequest found'})
|
||||
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestsArchive> {
|
||||
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestViewDto> {
|
||||
try{
|
||||
return await this.leaveRequestsService.findOneArchived(id);
|
||||
}catch {
|
||||
|
|
|
|||
|
|
@ -18,54 +18,67 @@ export interface CsvRow {
|
|||
export class CsvExportService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async collectTransaction(period_id: number, companies: ExportCompany[]): Promise<CsvRow[]> {
|
||||
const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||
async collectTransaction( period_id: number, companies: ExportCompany[], approved: boolean = true):
|
||||
Promise<CsvRow[]> {
|
||||
|
||||
const company_codes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||
|
||||
const period = await this.prisma.payPeriods.findFirst({
|
||||
where: { pay_period_no: period_id },
|
||||
});
|
||||
if(!period) {
|
||||
throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||
}
|
||||
if(!period) throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||
|
||||
const startDate = period.period_start;
|
||||
const endDate = period.period_end;
|
||||
const start_date = period.period_start;
|
||||
const end_date = period.period_end;
|
||||
|
||||
const approved_filter = approved ? { is_approved: true } : {};
|
||||
|
||||
//fetching shifts
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: { date: { gte: startDate, lte: endDate },
|
||||
timesheet: { employee: { company_code: { in: companyCodes} } },
|
||||
where: {
|
||||
date: { gte: start_date, lte: end_date },
|
||||
...approved_filter,
|
||||
timesheet: {
|
||||
employee: { company_code: { in: company_codes} } },
|
||||
},
|
||||
include: { bank_code: true,
|
||||
timesheet: { include: {employee: { include: { user:true,
|
||||
supervisor: { include: { user:true } },
|
||||
}},
|
||||
}},
|
||||
include: {
|
||||
bank_code: true,
|
||||
timesheet: { include: {
|
||||
employee: { include: {
|
||||
user:true,
|
||||
supervisor: { include: {
|
||||
user:true } } } } } },
|
||||
},
|
||||
});
|
||||
|
||||
//fetching expenses
|
||||
const expenses = await this.prisma.expenses.findMany({
|
||||
where: { date: { gte: startDate, lte: endDate },
|
||||
timesheet: { employee: { company_code: { in: companyCodes} } },
|
||||
where: {
|
||||
date: { gte: start_date, lte: end_date },
|
||||
...approved_filter,
|
||||
timesheet: { employee: { company_code: { in: company_codes} } },
|
||||
},
|
||||
include: { bank_code: true,
|
||||
timesheet: { include: { employee: { include: { user: true,
|
||||
supervisor: { include: { user:true } },
|
||||
} },
|
||||
} },
|
||||
},
|
||||
include: { bank_code: true,
|
||||
timesheet: { include: {
|
||||
employee: { include: {
|
||||
user: true,
|
||||
supervisor: { include: {
|
||||
user:true } } } } } },
|
||||
},
|
||||
});
|
||||
|
||||
//fetching leave requests
|
||||
const leaves = await this.prisma.leaveRequests.findMany({
|
||||
where : { start_date_time: { gte: startDate, lte: endDate },
|
||||
employee: { company_code: { in: companyCodes } },
|
||||
where : {
|
||||
start_date_time: { gte: start_date, lte: end_date },
|
||||
employee: { company_code: { in: company_codes } },
|
||||
},
|
||||
include: { bank_code: true,
|
||||
employee: { include: { user: true,
|
||||
supervisor: { include: { user: true } },
|
||||
}},
|
||||
include: {
|
||||
bank_code: true,
|
||||
employee: { include: {
|
||||
user: true,
|
||||
supervisor: { include: {
|
||||
user: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -74,7 +87,7 @@ export class CsvExportService {
|
|||
//Shifts Mapping
|
||||
for (const shift of shifts) {
|
||||
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);
|
||||
|
||||
rows.push({
|
||||
|
|
@ -85,7 +98,7 @@ export class CsvExportService {
|
|||
quantity_hours: hours,
|
||||
amount: undefined,
|
||||
week_number,
|
||||
pay_date: this.formatDate(endDate),
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
|
|
@ -93,7 +106,7 @@ export class CsvExportService {
|
|||
//Expenses Mapping
|
||||
for (const e of expenses) {
|
||||
const emp = e.timesheet.employee;
|
||||
const week_number = this.computeWeekNumber(startDate, e.date);
|
||||
const week_number = this.computeWeekNumber(start_date, e.date);
|
||||
|
||||
rows.push({
|
||||
company_code: emp.company_code,
|
||||
|
|
@ -103,7 +116,7 @@ export class CsvExportService {
|
|||
quantity_hours: undefined,
|
||||
amount: Number(e.amount),
|
||||
week_number,
|
||||
pay_date: this.formatDate(endDate),
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
|
|
@ -115,7 +128,7 @@ export class CsvExportService {
|
|||
const start = l.start_date_time;
|
||||
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);
|
||||
|
||||
rows.push({
|
||||
|
|
@ -126,7 +139,7 @@ export class CsvExportService {
|
|||
quantity_hours: hours,
|
||||
amount: undefined,
|
||||
week_number,
|
||||
pay_date: this.formatDate(endDate),
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
|
|
@ -146,7 +159,7 @@ export class CsvExportService {
|
|||
generateCsv(rows: CsvRow[]): Buffer {
|
||||
const header = [
|
||||
'company_code',
|
||||
'external_payrol_id',
|
||||
'external_payroll_id',
|
||||
'full_name',
|
||||
'bank_code',
|
||||
'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 { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||
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 { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -20,35 +21,35 @@ export class LeaveRequestController {
|
|||
@ApiOperation({summary: 'Create leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
||||
@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);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@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' })
|
||||
@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);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@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' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequests> {
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.findOne(id);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@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' })
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -58,14 +59,14 @@ export class LeaveRequestController {
|
|||
@ApiOperation({summary: 'Delete leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request deleted',type: CreateLeaveRequestsDto })
|
||||
@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);
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
@Patch(':id/approval')
|
||||
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 ?
|
||||
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
|
||||
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
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 {
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'Leave request`s unique id(auto-incremented)',
|
||||
})
|
||||
id: number;
|
||||
|
||||
//remove emp_id and use email
|
||||
@ApiProperty({
|
||||
example: '4655867',
|
||||
description: 'Employee`s id',
|
||||
})
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
employee_id: number;
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 7,
|
||||
|
|
@ -37,18 +26,17 @@ export class CreateLeaveRequestsDto {
|
|||
@ApiProperty({
|
||||
example: '22/06/2463',
|
||||
description: 'Leave request`s start date',
|
||||
})
|
||||
@Type(()=>Date)
|
||||
@IsNotEmpty()
|
||||
start_date_time:Date;
|
||||
})
|
||||
@IsISO8601()
|
||||
start_date_time:string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '25/03/3019',
|
||||
description: 'Leave request`s end date',
|
||||
})
|
||||
@Type(()=>Date)
|
||||
@IsOptional()
|
||||
end_date_time?: Date;
|
||||
@IsISO8601()
|
||||
end_date_time?: string;
|
||||
|
||||
@ApiProperty({
|
||||
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 { IsOptional, IsInt, IsEnum, IsDateString } from "class-validator";
|
||||
import { IsOptional, IsInt, IsEnum, IsDateString, IsEmail } from "class-validator";
|
||||
|
||||
export class SearchLeaveRequestsDto {
|
||||
//remove emp_id and use email
|
||||
@IsOptional()
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
employee_id?: number;
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(()=> Number)
|
||||
|
|
@ -20,9 +18,13 @@ export class SearchLeaveRequestsDto {
|
|||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
start_date?: Date;
|
||||
start_date?: string;
|
||||
|
||||
@IsOptional()
|
||||
@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 { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
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()
|
||||
export class LeaveRequestsService {
|
||||
|
|
@ -18,138 +25,93 @@ export class LeaveRequestsService {
|
|||
private readonly sickLeaveService: SickLeaveService
|
||||
) {}
|
||||
|
||||
//remove emp_id and use email
|
||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequests> {
|
||||
const { employee_id, bank_code_id, leave_type, start_date_time,
|
||||
end_date_time, comment, approval_status } = dto;
|
||||
|
||||
return this.prisma.leaveRequests.create({
|
||||
data: { employee_id, bank_code_id, leave_type, start_date_time,
|
||||
end_date_time, comment, approval_status: approval_status ?? undefined
|
||||
},
|
||||
include: { employee: { include: { user: true } },
|
||||
bank_code: true
|
||||
},
|
||||
//function to avoid using employee_id as identifier in the frontend.
|
||||
private async resolveEmployeeIdByEmail(email: string): Promise<number> {
|
||||
const employee = await this.prisma.employees.findFirst({
|
||||
where: { user: { email} },
|
||||
select: { id:true },
|
||||
});
|
||||
if(!employee) throw new NotFoundException(`Employee with email ${email} not found`);
|
||||
return employee.id;
|
||||
}
|
||||
|
||||
async findAll(filters: SearchLeaveRequestsDto): Promise<any[]> {
|
||||
const {start_date, end_date, ...other_filters } = filters;
|
||||
const where: Record<string, any> = buildPrismaWhere(other_filters);
|
||||
|
||||
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 },
|
||||
//create a leave-request without the use of employee_id
|
||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
const employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
...(employee_id !== undefined && { employee_id }),
|
||||
...(leave_type !== undefined && { leave_type } ),
|
||||
...(start_date_time !== undefined && { start_date_time }),
|
||||
...(end_date_time !== undefined && { end_date_time }),
|
||||
...(comment !== undefined && { comment }),
|
||||
...(approval_status !== undefined && { approval_status }),
|
||||
employee_id,
|
||||
bank_code_id: dto.bank_code_id,
|
||||
leave_type: dto.leave_type,
|
||||
start_date_time: new Date(dto.start_date_time),
|
||||
end_date_time: dto.end_date_time ? new Date(dto.end_date_time) : null,
|
||||
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> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.leaveRequests.delete({
|
||||
where: { id },
|
||||
//fetches all leave-requests using email
|
||||
async findAll(filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||
const {start_date, end_date,email, leave_type, approval_status, bank_code_id } = filters;
|
||||
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 ******************************************************
|
||||
|
|
@ -184,14 +146,30 @@ export class LeaveRequestsService {
|
|||
});
|
||||
}
|
||||
|
||||
//fetches all archived employees
|
||||
//fetches all archived leave-requests
|
||||
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
||||
return this.prisma.leaveRequestsArchive.findMany();
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
//fetches an archived employee
|
||||
async findOneArchived(id: number): Promise<LeaveRequestsArchive> {
|
||||
return this.prisma.leaveRequestsArchive.findUniqueOrThrow({ where: { id } });
|
||||
async findOneArchived(id: number): Promise<LeaveRequestViewDto> {
|
||||
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 { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
||||
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 { TimesheetsCommandService } from '../services/timesheets-command.service';
|
||||
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||
|
||||
@ApiTags('Timesheets')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -15,8 +16,8 @@ import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
|||
@Controller('timesheets')
|
||||
export class TimesheetsController {
|
||||
constructor(
|
||||
private readonly timesheetsService: TimesheetsQueryService,
|
||||
private readonly timesheetsCommandService: TimesheetsCommandService,
|
||||
private readonly timesheetsQuery: TimesheetsQueryService,
|
||||
private readonly timesheetsCommand: TimesheetsCommandService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
|
|
@ -25,17 +26,18 @@ export class TimesheetsController {
|
|||
@ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
|
||||
return this.timesheetsService.create(dto);
|
||||
return this.timesheetsQuery.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find all timesheets' })
|
||||
@ApiResponse({ status: 201, description: 'List of timesheet found', type: CreateTimesheetDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of timesheets not found' })
|
||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchTimesheetDto): Promise<any[]> {
|
||||
return this.timesheetsService.findAll(filters);
|
||||
async getPeriodByQuery(
|
||||
@Query('year', ParseIntPipe ) year: number,
|
||||
@Query('period_no', ParseIntPipe ) period_no: number,
|
||||
@Query('email') email?: string
|
||||
): Promise<TimesheetPeriodDto> {
|
||||
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')
|
||||
|
|
@ -44,7 +46,7 @@ export class TimesheetsController {
|
|||
@ApiResponse({ status: 201, description: 'Timesheet found', type: CreateTimesheetDto })
|
||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
return this.timesheetsService.findOne(id);
|
||||
return this.timesheetsQuery.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
|
|
@ -56,7 +58,7 @@ export class TimesheetsController {
|
|||
@Param('id', ParseIntPipe) id:number,
|
||||
@Body() dto: UpdateTimesheetDto,
|
||||
): Promise<Timesheets> {
|
||||
return this.timesheetsService.update(id, dto);
|
||||
return this.timesheetsQuery.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
|
|
@ -65,12 +67,12 @@ export class TimesheetsController {
|
|||
@ApiResponse({ status: 201, description: 'Timesheet deleted', type: CreateTimesheetDto })
|
||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
return this.timesheetsService.remove(id);
|
||||
return this.timesheetsQuery.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.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 { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
||||
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()
|
||||
export class TimesheetsQueryService {
|
||||
|
|
@ -30,38 +34,63 @@ export class TimesheetsQueryService {
|
|||
});
|
||||
}
|
||||
|
||||
async findAll(filters: SearchTimesheetDto): Promise<any[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
|
||||
//fetchs lists of shifts and expenses for a selected timesheet
|
||||
const rawlist = await this.prisma.timesheets.findMany({
|
||||
where, include: {
|
||||
shift: { include: {bank_code: true } },
|
||||
expense: { include: { bank_code: true } },
|
||||
employee: { include: { user : true } },
|
||||
},
|
||||
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
||||
//finds the employee
|
||||
const employee = await this.prisma.employees.findFirst({
|
||||
where: { user: { is: { email } } },
|
||||
select: { id: true },
|
||||
});
|
||||
if(!employee) throw new NotFoundException(`no employee with email ${email} found`);
|
||||
|
||||
const detailedlist = await Promise.all(
|
||||
rawlist.map(async timesheet => {
|
||||
//detailed shifts
|
||||
const detailedShifts = timesheet.shift.map(shift => {
|
||||
const totalhours = computeHours(shift.start_time, shift.end_time, ROUND_TO);
|
||||
const regularHours = Math.min(8, totalhours);
|
||||
const dailyOvertime = this.overtime.getDailyOvertimeHours(shift.start_time, shift.end_time);
|
||||
const payRegular = regularHours * shift.bank_code.modifier;
|
||||
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, shift.bank_code.modifier);
|
||||
return { ...shift, totalhours, payRegular, payOvertime };
|
||||
});
|
||||
//calculate overtime weekly
|
||||
const weeklyOvertimeHours = detailedShifts.length
|
||||
? await this.overtime.getWeeklyOvertimeHours(
|
||||
timesheet.employee_id,
|
||||
timesheet.shift[0].date): 0;
|
||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||
//finds the period
|
||||
const period = await this.prisma.payPeriods.findFirst({
|
||||
where: { pay_year: year, pay_period_no: period_no },
|
||||
select: { period_start: true, period_end: true },
|
||||
});
|
||||
if(!period) throw new NotFoundException(`Period ${year}-${period_no} not found`);
|
||||
|
||||
const from = toUTCDateOnly(period.period_start);
|
||||
const to = endOfDayUTC(period.period_end);
|
||||
|
||||
//collects data from shifts and expenses
|
||||
const [ raw_shifts, raw_expenses] = await Promise.all([
|
||||
this.prisma.shifts.findMany({
|
||||
where: {
|
||||
timesheet: { is: { employee_id: employee.id } },
|
||||
date: { gte: from, lte: to },
|
||||
},
|
||||
select: { date: true,start_time: true, end_time: true, is_approved: true },
|
||||
orderBy: [{date: 'asc'}, { start_time: 'asc' }],
|
||||
}),
|
||||
);
|
||||
return detailedlist;
|
||||
this.prisma.expenses.findMany({
|
||||
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> {
|
||||
|
|
|
|||
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