refactor(leave-request): refactor to use email instead of employee ids

This commit is contained in:
Matthieu Haineault 2025-08-21 15:00:40 -04:00
parent bdf6662374
commit 9a150715b0
9 changed files with 243 additions and 156 deletions

View File

@ -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,
@ -38,17 +27,16 @@ export class CreateLeaveRequestsDto {
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',

View 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;
}

View File

@ -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;
}

View File

@ -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,
}
}

View 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}`
}
}

View File

@ -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);
}
}

View 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;
}

View File

@ -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}>;

View 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}>;