From 791b3aacb93f46d49f6a8b2db65fac64d40651c6 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Fri, 9 Jan 2026 11:39:30 -0500 Subject: [PATCH] feat(employees): add paid time off to properties sent to employee list queries (some partial, full when editing) --- .../employees/employee-detailed.dto.ts | 2 + .../services/employees-get.service.ts | 385 +++++++++--------- .../domains/paid-time-off.dto.ts | 11 + 3 files changed, 214 insertions(+), 184 deletions(-) create mode 100644 src/time-and-attendance/domains/paid-time-off.dto.ts diff --git a/src/identity-and-account/employees/employee-detailed.dto.ts b/src/identity-and-account/employees/employee-detailed.dto.ts index 4ed150e..2e03d92 100644 --- a/src/identity-and-account/employees/employee-detailed.dto.ts +++ b/src/identity-and-account/employees/employee-detailed.dto.ts @@ -1,5 +1,6 @@ import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; import { Type } from 'class-transformer'; +import { PaidTimeOffDto } from 'src/time-and-attendance/domains/paid-time-off.dto'; export class EmployeeDetailedDto { @IsString() @IsNotEmpty() first_name: string; @@ -15,6 +16,7 @@ export class EmployeeDetailedDto { @IsInt() daily_expected_hours: number; @IsDateString() @IsOptional() last_work_day?: string | null; @IsString() @IsOptional() residence?: string; + @IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: Partial | null; @IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number; @IsArray() @IsString({ each: true }) user_module_access: string[]; @IsInt() @IsOptional() preset_id?: number; diff --git a/src/identity-and-account/employees/services/employees-get.service.ts b/src/identity-and-account/employees/services/employees-get.service.ts index 31f7558..40e24bb 100644 --- a/src/identity-and-account/employees/services/employees-get.service.ts +++ b/src/identity-and-account/employees/services/employees-get.service.ts @@ -12,201 +12,218 @@ import { toKeysFromBoolean } from "src/common/utils/boolean-utils"; @Injectable() export class EmployeesGetService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - ) { } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } - async findListEmployees(): Promise[], string>> { - const employee_list = await this.prisma.employees.findMany({ - select: { - user: { - select: { - first_name: true, - last_name: true, - email: true, - }, - }, - supervisor: { - select: { - user: { - select: { - first_name: true, - last_name: true, - }, - }, - }, - }, - is_supervisor: true, - daily_expected_hours: true, - job_title: true, - company_code: true, - external_payroll_id: true, - first_work_day: true, - last_work_day: true, - schedule_preset_id: true, + async findListEmployees(): Promise[], string>> { + const employee_list = await this.prisma.employees.findMany({ + select: { + user: { + select: { + first_name: true, + last_name: true, + email: true, + phone_number: true, + }, + }, + supervisor: { + select: { + user: { + select: { + first_name: true, + last_name: true, + }, + }, + }, + }, + is_supervisor: true, + daily_expected_hours: true, + job_title: true, + company_code: true, + external_payroll_id: true, + first_work_day: true, + last_work_day: true, + schedule_preset_id: true, - } - }).then(rows => rows.map(r => ({ - first_name: r.user.first_name, - last_name: r.user.last_name, - email: r.user.email, - company_name: toStringFromCompanyCode(r.company_code), - job_title: r.job_title ?? '', - daily_expected_hours: r.daily_expected_hours, - external_payroll_id: r.external_payroll_id, - employee_full_name: `${r.user.first_name} ${r.user.last_name}`, - is_supervisor: r.is_supervisor, - supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`, - first_work_day: toStringFromDate(r.first_work_day), - last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null, - preset_id: r.schedule_preset_id ?? undefined, - }))); - return { success: true, data: employee_list }; - }; + } + }).then(rows => rows.map(r => ({ + first_name: r.user.first_name, + last_name: r.user.last_name, + email: r.user.email, + phone_number: r.user.phone_number, + company_name: toStringFromCompanyCode(r.company_code), + job_title: r.job_title ?? '', + daily_expected_hours: r.daily_expected_hours, + external_payroll_id: r.external_payroll_id, + employee_full_name: `${r.user.first_name} ${r.user.last_name}`, + is_supervisor: r.is_supervisor, + supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`, + first_work_day: toStringFromDate(r.first_work_day), + last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null, + preset_id: r.schedule_preset_id ?? undefined, + }))); + return { success: true, data: employee_list }; + }; - async findOwnProfile(email: string): Promise, string>> { - const user_id = await this.emailResolver.resolveUserIdWithEmail(email); - if (!user_id.success) return { success: false, error: 'INVALID_USER' }; + async findOwnProfile(email: string): Promise, string>> { + const user_id = await this.emailResolver.resolveUserIdWithEmail(email); + if (!user_id.success) return { success: false, error: 'INVALID_USER' }; - const existing_profile = await this.prisma.employees.findUnique({ - where: { user_id: user_id.data }, - select: { - user: { - select: { - first_name: true, - last_name: true, - email: true, - phone_number: true, - residence: true, - }, - }, - first_work_day: true, - company_code: true, - job_title: true, - external_payroll_id: true, - is_supervisor: true, - schedule_preset_id: true, - daily_expected_hours: true, - supervisor: { - select: { - id: true, user: { - select: { - first_name: true, - last_name: true, - }, - }, - }, - }, - }, - }); - if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + const existing_profile = await this.prisma.employees.findUnique({ + where: { user_id: user_id.data }, + select: { + user: { + select: { + first_name: true, + last_name: true, + email: true, + phone_number: true, + residence: true, + }, + }, + first_work_day: true, + company_code: true, + job_title: true, + external_payroll_id: true, + paid_time_off: true, + is_supervisor: true, + schedule_preset_id: true, + daily_expected_hours: true, + supervisor: { + select: { + id: true, user: { + select: { + first_name: true, + last_name: true, + }, + }, + }, + }, + }, + }); + if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; - const company_name = toStringFromCompanyCode(existing_profile.company_code); + const company_name = toStringFromCompanyCode(existing_profile.company_code); - return { - success: true, data: { - first_name: existing_profile.user.first_name, - last_name: existing_profile.user.last_name, - email: existing_profile.user.email, - supervisor_full_name: `${existing_profile.supervisor?.user.first_name} ${existing_profile.supervisor?.user.last_name}`, - company_name: company_name, - daily_expected_hours: existing_profile.daily_expected_hours, - job_title: existing_profile.job_title ?? '', - external_payroll_id: existing_profile.external_payroll_id, - is_supervisor: existing_profile.is_supervisor, - phone_number: existing_profile.user.phone_number, - residence: existing_profile.user.phone_number, - first_work_day: toStringFromDate(existing_profile.first_work_day), - preset_id: existing_profile.schedule_preset_id ?? undefined, - }, - }; - }; + return { + success: true, data: { + first_name: existing_profile.user.first_name, + last_name: existing_profile.user.last_name, + email: existing_profile.user.email, + supervisor_full_name: `${existing_profile.supervisor?.user.first_name} ${existing_profile.supervisor?.user.last_name}`, + company_name: company_name, + daily_expected_hours: existing_profile.daily_expected_hours, + job_title: existing_profile.job_title ?? '', + external_payroll_id: existing_profile.external_payroll_id, + paid_time_off: { + vacation_hours: existing_profile.paid_time_off?.vacation_hours.toNumber() ?? 0, + banked_hours: existing_profile.paid_time_off?.banked_hours.toNumber() ?? 0, + last_updated: existing_profile.paid_time_off?.last_updated.toISOString().slice(0, 10) ?? '', + }, + is_supervisor: existing_profile.is_supervisor, + phone_number: existing_profile.user.phone_number, + residence: existing_profile.user.phone_number, + first_work_day: toStringFromDate(existing_profile.first_work_day), + preset_id: existing_profile.schedule_preset_id ?? undefined, + }, + }; + }; - async findOneDetailedProfile(email: string, employee_email?: string): Promise> { - const account_email = employee_email ?? email; - const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); - if (!user_id.success) return { success: false, error: 'INVALID_USER' }; + async findOneDetailedProfile(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'INVALID_USER' }; - const employee = await this.prisma.employees.findUnique({ - where: { user_id: user_id.data }, - select: { - user: { - select: { - first_name: true, - last_name: true, - email: true, - phone_number: true, - residence: true, - user_module_access: { - select: { - dashboard: true, - employee_list: true, - employee_management: true, - personal_profile: true, - timesheets: true, - timesheets_approval: true, - }, - }, - }, - }, - supervisor: { - select: { - user: { - select: { - first_name: true, - last_name: true, - }, - }, - }, - }, - job_title: true, - company_code: true, - first_work_day: true, - last_work_day: true, - external_payroll_id: true, - is_supervisor: true, - daily_expected_hours: true, - schedule_preset_id: true, - schedule_preset: { - select: { - id: true, - } - } - } - }); - if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }; - if (!employee.user) return { success: false, error: 'USER_NOT_FOUND' }; + const employee = await this.prisma.employees.findUnique({ + where: { user_id: user_id.data }, + select: { + user: { + select: { + first_name: true, + last_name: true, + email: true, + phone_number: true, + residence: true, + user_module_access: { + select: { + dashboard: true, + employee_list: true, + employee_management: true, + personal_profile: true, + timesheets: true, + timesheets_approval: true, + }, + }, + }, + }, + supervisor: { + select: { + user: { + select: { + first_name: true, + last_name: true, + }, + }, + }, + }, + job_title: true, + company_code: true, + first_work_day: true, + last_work_day: true, + external_payroll_id: true, + paid_time_off: true, + is_supervisor: true, + daily_expected_hours: true, + schedule_preset_id: true, + schedule_preset: { + select: { + id: true, + } + } + } + }); + if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }; + if (!employee.user) return { success: false, error: 'USER_NOT_FOUND' }; - let module_access_array: Modules[] = []; - if (employee.user.user_module_access) { - module_access_array = toStringFromBoolean(employee.user.user_module_access); - } + let module_access_array: Modules[] = []; + if (employee.user.user_module_access) { + module_access_array = toStringFromBoolean(employee.user.user_module_access); + } - const company_name = toStringFromCompanyCode(employee.company_code); + const company_name = toStringFromCompanyCode(employee.company_code); - return { - success: true, - data: { - first_name: employee.user.first_name, - last_name: employee.user.last_name, - email: employee.user.email, - residence: employee.user.residence ?? '', - phone_number: employee.user.phone_number, - company_name: company_name, - is_supervisor: employee.is_supervisor ?? false, - job_title: employee.job_title ?? '', - external_payroll_id: employee.external_payroll_id, - employee_full_name: `${employee.user.first_name} ${employee.user.last_name}`, - first_work_day: toStringFromDate(employee.first_work_day), - last_work_day: employee.last_work_day ? toStringFromDate(employee.last_work_day) : undefined, - supervisor_full_name: employee.supervisor ? `${employee.supervisor?.user.first_name} ${employee.supervisor?.user.last_name}` : '', - user_module_access: module_access_array, - daily_expected_hours: employee.daily_expected_hours, - preset_id: employee.schedule_preset_id ? employee.schedule_preset_id : undefined, - }, - }; - }; + return { + success: true, + data: { + first_name: employee.user.first_name, + last_name: employee.user.last_name, + email: employee.user.email, + residence: employee.user.residence ?? '', + phone_number: employee.user.phone_number, + company_name: company_name, + is_supervisor: employee.is_supervisor ?? false, + job_title: employee.job_title ?? '', + external_payroll_id: employee.external_payroll_id, + paid_time_off: { + id: employee.paid_time_off?.id ?? -1, + employee_id: employee.paid_time_off?.employee_id ?? -1, + sick_hours: employee.paid_time_off?.sick_hours.toNumber() ?? 0, + vacation_hours: employee.paid_time_off?.vacation_hours.toNumber() ?? 0, + banked_hours: employee.paid_time_off?.banked_hours.toNumber() ?? 0, + last_updated: employee.paid_time_off?.last_updated.toISOString().slice(0, 10) ?? '', + }, + employee_full_name: `${employee.user.first_name} ${employee.user.last_name}`, + first_work_day: toStringFromDate(employee.first_work_day), + last_work_day: employee.last_work_day ? toStringFromDate(employee.last_work_day) : undefined, + supervisor_full_name: employee.supervisor ? `${employee.supervisor?.user.first_name} ${employee.supervisor?.user.last_name}` : '', + user_module_access: module_access_array, + daily_expected_hours: employee.daily_expected_hours, + preset_id: employee.schedule_preset_id ? employee.schedule_preset_id : undefined, + }, + }; + }; } const createDefaultModuleAccess = (): Record => diff --git a/src/time-and-attendance/domains/paid-time-off.dto.ts b/src/time-and-attendance/domains/paid-time-off.dto.ts new file mode 100644 index 0000000..8f56175 --- /dev/null +++ b/src/time-and-attendance/domains/paid-time-off.dto.ts @@ -0,0 +1,11 @@ +import { Type } from "class-transformer"; +import { IsDateString, IsDecimal, IsInt, IsNotEmpty, IsNumber, IsOptional } from "class-validator"; + +export class PaidTimeOffDto { + @IsInt() @IsNotEmpty() id: number; + @IsInt() @IsNotEmpty() employee_id: number; + @IsOptional() @Type(() => Number) vacation_hours?: number; + @IsOptional() @Type(() => Number) sick_hours?: number; + @IsOptional() @Type(() => Number) banked_hours?: number; + @IsDateString() @IsOptional() last_updated: string; +} \ No newline at end of file