From 9a150715b0824c4875d8c0c7e30d566cc7307329 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 21 Aug 2025 15:00:40 -0400 Subject: [PATCH] refactor(leave-request): refactor to use email instead of employee ids --- .../dtos/create-leave-request.dto.ts | 30 +-- .../dtos/leave-request.view.dto.ts | 13 + .../dtos/search-leave-request.dto.ts | 20 +- .../mappers/leave-requests-archive.mapper.ts | 17 ++ .../mappers/leave-requests.mapper.ts | 19 ++ .../services/leave-requests.service.ts | 230 ++++++++---------- .../utils/leave-request.transform.ts | 32 +++ .../utils/leave-requests-archive.select.ts | 16 ++ .../utils/leave-requests.select.ts | 22 ++ 9 files changed, 243 insertions(+), 156 deletions(-) create mode 100644 src/modules/leave-requests/dtos/leave-request.view.dto.ts create mode 100644 src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts create mode 100644 src/modules/leave-requests/mappers/leave-requests.mapper.ts create mode 100644 src/modules/leave-requests/utils/leave-request.transform.ts create mode 100644 src/modules/leave-requests/utils/leave-requests-archive.select.ts create mode 100644 src/modules/leave-requests/utils/leave-requests.select.ts diff --git a/src/modules/leave-requests/dtos/create-leave-request.dto.ts b/src/modules/leave-requests/dtos/create-leave-request.dto.ts index 676f432..136c858 100644 --- a/src/modules/leave-requests/dtos/create-leave-request.dto.ts +++ b/src/modules/leave-requests/dtos/create-leave-request.dto.ts @@ -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', diff --git a/src/modules/leave-requests/dtos/leave-request.view.dto.ts b/src/modules/leave-requests/dtos/leave-request.view.dto.ts new file mode 100644 index 0000000..693368d --- /dev/null +++ b/src/modules/leave-requests/dtos/leave-request.view.dto.ts @@ -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; +} \ No newline at end of file diff --git a/src/modules/leave-requests/dtos/search-leave-request.dto.ts b/src/modules/leave-requests/dtos/search-leave-request.dto.ts index 85bd95b..15ce8e4 100644 --- a/src/modules/leave-requests/dtos/search-leave-request.dto.ts +++ b/src/modules/leave-requests/dtos/search-leave-request.dto.ts @@ -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; } \ No newline at end of file diff --git a/src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts b/src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts new file mode 100644 index 0000000..48d91e0 --- /dev/null +++ b/src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts @@ -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, + } +} \ No newline at end of file diff --git a/src/modules/leave-requests/mappers/leave-requests.mapper.ts b/src/modules/leave-requests/mappers/leave-requests.mapper.ts new file mode 100644 index 0000000..4fe2133 --- /dev/null +++ b/src/modules/leave-requests/mappers/leave-requests.mapper.ts @@ -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}` + } +} \ No newline at end of file diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index ed5e8bd..1231ed6 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -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 { - 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 { + 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 { - const {start_date, end_date, ...other_filters } = filters; - const where: Record = 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 { - 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 { - 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 { + 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 { - await this.findOne(id); - return this.prisma.leaveRequests.delete({ - where: { id }, + //fetches all leave-requests using email + async findAll(filters: SearchLeaveRequestsDto): Promise { + 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 { + 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 { + await this.findOne(id); + const data: Record = {}; + + 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 { + 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 { return this.prisma.leaveRequestsArchive.findMany(); } //remove emp_id and use email //fetches an archived employee - async findOneArchived(id: number): Promise { - return this.prisma.leaveRequestsArchive.findUniqueOrThrow({ where: { id } }); + async findOneArchived(id: number): Promise { + 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); } } \ No newline at end of file diff --git a/src/modules/leave-requests/utils/leave-request.transform.ts b/src/modules/leave-requests/utils/leave-request.transform.ts new file mode 100644 index 0000000..b70c66d --- /dev/null +++ b/src/modules/leave-requests/utils/leave-request.transform.ts @@ -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; +} \ No newline at end of file diff --git a/src/modules/leave-requests/utils/leave-requests-archive.select.ts b/src/modules/leave-requests/utils/leave-requests-archive.select.ts new file mode 100644 index 0000000..5dbbd36 --- /dev/null +++ b/src/modules/leave-requests/utils/leave-requests-archive.select.ts @@ -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}>; \ No newline at end of file diff --git a/src/modules/leave-requests/utils/leave-requests.select.ts b/src/modules/leave-requests/utils/leave-requests.select.ts new file mode 100644 index 0000000..9636334 --- /dev/null +++ b/src/modules/leave-requests/utils/leave-requests.select.ts @@ -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}>;