fix(contracts): added a contract class to employee and timesheet overview dtos and ajusted queries

This commit is contained in:
Matthieu Haineault 2026-03-23 14:21:36 -04:00
parent c47dcb1f2f
commit a5bd7d54fe
13 changed files with 3036 additions and 79 deletions

4
.gitignore vendored
View File

@ -55,9 +55,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Generated prisma folders (from -> npm run prisma:generated)
prisma/mariadb/generated/prisma/client/mariadb/
prisma/postgres/generated/prisma/client/postgres/
prisma/prisma-legacy/generated/prisma/client/legacy/
!swagger-spec.json !swagger-spec.json

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,9 +88,9 @@ model Contracts {
employee_id Int @unique employee_id Int @unique
daily_expected_hours Int @default(24) daily_expected_hours Int @default(24)
applicable_overtime ApplicableOvertime[] @default([WEEKLY]) applicable_overtime ApplicableOvertime[] @default([WEEKLY])
phone_allocation Decimal @default(0.00) phone_allocation Decimal @default(0.00) @db.Decimal(5, 2)
on_call_allocation Decimal @default(0.00) on_call_allocation Decimal @default(0.00) @db.Decimal(5, 2)
weekend_on_call_allocation Decimal @default(0.00) weekend_on_call_allocation Decimal @default(0.00) @db.Decimal(5, 2)
employee Employees @relation("EmployeeContract", fields: [employee_id], references: [id]) employee Employees @relation("EmployeeContract", fields: [employee_id], references: [id])

View File

@ -5,6 +5,7 @@ import { ContractService } from "src/identity-and-account/contract/services/cont
export class ContractController { export class ContractController {
constructor(private readonly getService: ContractService) {} constructor(private readonly getService: ContractService) {}
} }

View File

@ -3,7 +3,6 @@ import { IsInt, IsString } from "class-validator";
import { ApplicableOvertime } from "prisma/postgres/generated/prisma/client/postgres/enums"; import { ApplicableOvertime } from "prisma/postgres/generated/prisma/client/postgres/enums";
export class Contract { export class Contract {
@IsInt() employee_id: number;
@IsInt() daily_expected_hours: number; @IsInt() daily_expected_hours: number;
@IsString() applicable_overtime: ApplicableOvertime; @IsString() applicable_overtime: ApplicableOvertime;
@Type(() => Number) phone_allocation: number; @Type(() => Number) phone_allocation: number;

View File

@ -2,6 +2,7 @@ import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptiona
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { PaidTimeOffDto } from 'src/time-and-attendance/paid-time-off/paid-time-off.dto'; import { PaidTimeOffDto } from 'src/time-and-attendance/paid-time-off/paid-time-off.dto';
import { Prisma } from 'prisma/postgres/generated/prisma/client/postgres/client'; import { Prisma } from 'prisma/postgres/generated/prisma/client/postgres/client';
import { Contract } from 'src/identity-and-account/contract/contract.dto';
export class EmployeeDetailedDto { export class EmployeeDetailedDto {
@IsString() @IsNotEmpty() first_name: string; @IsString() @IsNotEmpty() first_name: string;
@ -14,13 +15,13 @@ export class EmployeeDetailedDto {
@IsEmail() @IsOptional() email: string; @IsEmail() @IsOptional() email: string;
@IsString() phone_number: string; @IsString() phone_number: string;
@IsDateString() first_work_day: string; @IsDateString() first_work_day: string;
@IsInt() daily_expected_hours: number;
@IsDateString() @IsOptional() last_work_day?: string | null; @IsDateString() @IsOptional() last_work_day?: string | null;
@IsString() @IsOptional() residence?: string; @IsString() @IsOptional() residence?: string;
@IsInt() @Type(() => Contract) contract: Partial<Contract>;
@IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: Partial<PaidTimeOffDto>; @IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: Partial<PaidTimeOffDto>;
@IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number; @IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number;
@IsArray() @IsString({ each: true }) user_module_access: string[]; @IsArray() @IsString({ each: true }) user_module_access: string[];
@IsInt() @IsOptional() preset_id?: number; @IsInt() @IsOptional() preset_id?: number;
} }
export class EmployeeDetailedUpsertDto { export class EmployeeDetailedUpsertDto {
@ -34,13 +35,13 @@ export class EmployeeDetailedUpsertDto {
@IsEmail() @IsOptional() email: string; @IsEmail() @IsOptional() email: string;
@IsString() phone_number: string; @IsString() phone_number: string;
@IsDateString() first_work_day: string; @IsDateString() first_work_day: string;
@IsInt() daily_expected_hours: number;
@IsDateString() @IsOptional() last_work_day?: string | null; @IsDateString() @IsOptional() last_work_day?: string | null;
@IsString() @IsOptional() residence?: string; @IsString() @IsOptional() residence?: string;
@IsInt() @Type(() => Contract) contract: Partial<Contract>;
@IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: PaidTimeOffDto; @IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: PaidTimeOffDto;
@IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number; @IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number;
@IsArray() @IsString({ each: true }) user_module_access: string[]; @IsArray() @IsString({ each: true }) user_module_access: string[];
@IsInt() @IsOptional() preset_id?: number; @IsInt() @IsOptional() preset_id?: number;
} }
@ -74,9 +75,26 @@ export type EmployeeWithDetails = Prisma.EmployeesGetPayload<{
first_work_day: true, first_work_day: true,
last_work_day: true, last_work_day: true,
external_payroll_id: true, external_payroll_id: true,
paid_time_off: true, paid_time_off: {
select: {
id: true,
employee_id: true,
sick_hours: true,
vacation_hours: true,
banked_hours: true,
last_updated: true,
},
},
is_supervisor: true, is_supervisor: true,
daily_expected_hours: true, contracts: {
select: {
daily_expected_hours: true,
applicable_overtime: true,
weekend_on_call_allocation: true,
on_call_allocation: true,
phone_allocation: true,
},
},
schedule_preset_id: true, schedule_preset_id: true,
schedule_preset: { select: { id: true } } schedule_preset: { select: { id: true } }
} }

View File

@ -44,13 +44,20 @@ export class EmployeesCreateService {
data: { data: {
user_id: user.id, user_id: user.id,
external_payroll_id: dto.external_payroll_id, external_payroll_id: dto.external_payroll_id,
daily_expected_hours: dto.daily_expected_hours,
company_code: company_code, company_code: company_code,
job_title: dto.job_title, job_title: dto.job_title,
first_work_day: first_work_day, first_work_day: first_work_day,
is_supervisor: dto.is_supervisor, is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id, supervisor_id: supervisor_id,
schedule_preset_id: dto.preset_id, schedule_preset_id: dto.preset_id,
contracts: {
create: {
daily_expected_hours: dto.contract.daily_expected_hours,
phone_allocation: dto.contract.phone_allocation,
on_call_allocation: dto.contract.on_call_allocation,
weekend_on_call_allocation: dto.contract.weekend_on_call_allocation,
},
},
}, },
}); });
}); });

View File

@ -37,8 +37,12 @@ export class EmployeesGetService {
}, },
}, },
}, },
contracts: {
select: {
daily_expected_hours: true,
},
},
is_supervisor: true, is_supervisor: true,
daily_expected_hours: true,
job_title: true, job_title: true,
company_code: true, company_code: true,
external_payroll_id: true, external_payroll_id: true,
@ -47,23 +51,29 @@ export class EmployeesGetService {
schedule_preset_id: true, schedule_preset_id: true,
} }
}).then(rows => rows.map(r => ({ });
first_name: r.user.first_name,
last_name: r.user.last_name, const employeeDetailedList = employee_list.map(r => {
email: r.user.email, return {
phone_number: r.user.phone_number, first_name: r.user.first_name,
company_name: toStringFromCompanyCode(r.company_code), last_name: r.user.last_name,
job_title: r.job_title ?? '', email: r.user.email,
daily_expected_hours: r.daily_expected_hours, phone_number: r.user.phone_number,
external_payroll_id: r.external_payroll_id, company_name: toStringFromCompanyCode(r.company_code),
employee_full_name: `${r.user.first_name} ${r.user.last_name}`, job_title: r.job_title ?? '',
is_supervisor: r.is_supervisor, contract: {
supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`, daily_expected_hours: r.contracts?.daily_expected_hours ?? 24,
first_work_day: toStringFromDate(r.first_work_day), },
last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null, external_payroll_id: r.external_payroll_id,
preset_id: r.schedule_preset_id ?? undefined, employee_full_name: `${r.user.first_name} ${r.user.last_name}`,
}))); is_supervisor: r.is_supervisor,
return { success: true, data: employee_list }; 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: employeeDetailedList };
}; };
async findOwnProfile(email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> { async findOwnProfile(email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
@ -89,7 +99,11 @@ export class EmployeesGetService {
paid_time_off: true, paid_time_off: true,
is_supervisor: true, is_supervisor: true,
schedule_preset_id: true, schedule_preset_id: true,
daily_expected_hours: true, contracts: {
select: {
daily_expected_hours: true,
},
},
supervisor: { supervisor: {
select: { select: {
id: true, user: { id: true, user: {
@ -103,6 +117,7 @@ export class EmployeesGetService {
}, },
}); });
if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
if (!existing_profile.contracts) return { success: false, error: 'CONTRACT_NOT_FOUND' };
const company_name = toStringFromCompanyCode(existing_profile.company_code); const company_name = toStringFromCompanyCode(existing_profile.company_code);
@ -113,7 +128,9 @@ export class EmployeesGetService {
email: existing_profile.user.email, email: existing_profile.user.email,
supervisor_full_name: `${existing_profile.supervisor?.user.first_name} ${existing_profile.supervisor?.user.last_name}`, supervisor_full_name: `${existing_profile.supervisor?.user.first_name} ${existing_profile.supervisor?.user.last_name}`,
company_name: company_name, company_name: company_name,
daily_expected_hours: existing_profile.daily_expected_hours, contract: {
daily_expected_hours: existing_profile.contracts?.daily_expected_hours ?? 24,
},
job_title: existing_profile.job_title ?? '', job_title: existing_profile.job_title ?? '',
external_payroll_id: existing_profile.external_payroll_id, external_payroll_id: existing_profile.external_payroll_id,
paid_time_off: { paid_time_off: {
@ -170,6 +187,11 @@ export class EmployeesGetService {
}, },
}, },
}, },
contracts: {
select: {
daily_expected_hours: true,
}
},
job_title: true, job_title: true,
company_code: true, company_code: true,
first_work_day: true, first_work_day: true,
@ -177,7 +199,6 @@ export class EmployeesGetService {
external_payroll_id: true, external_payroll_id: true,
paid_time_off: true, paid_time_off: true,
is_supervisor: true, is_supervisor: true,
daily_expected_hours: true,
schedule_preset_id: true, schedule_preset_id: true,
schedule_preset: { schedule_preset: {
select: { select: {
@ -196,35 +217,36 @@ export class EmployeesGetService {
const company_name = toStringFromCompanyCode(employee.company_code); const company_name = toStringFromCompanyCode(employee.company_code);
return { const detailed_employee: EmployeeDetailedDto = {
success: true, first_name: employee.user.first_name,
data: { last_name: employee.user.last_name,
first_name: employee.user.first_name, email: employee.user.email,
last_name: employee.user.last_name, residence: employee.user.residence ?? '',
email: employee.user.email, phone_number: employee.user.phone_number,
residence: employee.user.residence ?? '', company_name: company_name,
phone_number: employee.user.phone_number, is_supervisor: employee.is_supervisor ?? false,
company_name: company_name, job_title: employee.job_title ?? '',
is_supervisor: employee.is_supervisor ?? false, external_payroll_id: employee.external_payroll_id,
job_title: employee.job_title ?? '', paid_time_off: {
external_payroll_id: employee.external_payroll_id, id: employee.paid_time_off?.id ?? -1,
paid_time_off: { employee_id: employee.paid_time_off?.employee_id ?? -1,
id: employee.paid_time_off?.id ?? -1, sick_hours: employee.paid_time_off?.sick_hours.toNumber() ?? 0,
employee_id: employee.paid_time_off?.employee_id ?? -1, vacation_hours: employee.paid_time_off?.vacation_hours.toNumber() ?? 0,
sick_hours: employee.paid_time_off?.sick_hours.toNumber() ?? 0, banked_hours: employee.paid_time_off?.banked_hours.toNumber() ?? 0,
vacation_hours: employee.paid_time_off?.vacation_hours.toNumber() ?? 0, last_updated: employee.paid_time_off?.last_updated?.toISOString() ?? null,
banked_hours: employee.paid_time_off?.banked_hours.toNumber() ?? 0,
last_updated: employee.paid_time_off?.last_updated?.toISOString() ?? null,
},
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,
}, },
}; 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,
contract: {
daily_expected_hours: employee.contracts?.daily_expected_hours ?? 24,
},
preset_id: employee.schedule_preset_id ? employee.schedule_preset_id : undefined,
}
return { success: true, data: detailed_employee };
}; };
} }

View File

@ -98,13 +98,20 @@ export class EmployeesUpdateService {
data: { data: {
company_code: company_code, company_code: company_code,
job_title: dto.job_title, job_title: dto.job_title,
daily_expected_hours: dto.daily_expected_hours,
first_work_day: toDateFromString(dto.first_work_day), first_work_day: toDateFromString(dto.first_work_day),
last_work_day: last_work_day, last_work_day: last_work_day,
is_supervisor: dto.is_supervisor, is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id, supervisor_id: supervisor_id,
schedule_preset_id: dto.preset_id, schedule_preset_id: dto.preset_id,
external_payroll_id: dto.external_payroll_id, external_payroll_id: dto.external_payroll_id,
contracts: {
update: {
daily_expected_hours: dto.contract.daily_expected_hours,
on_call_allocation: dto.contract.on_call_allocation,
weekend_on_call_allocation: dto.contract.weekend_on_call_allocation,
phone_allocation: dto.contract.phone_allocation,
},
},
}, },
}); });

View File

@ -1,4 +1,3 @@
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils"; import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
@ -16,9 +15,9 @@ export class GetTimesheetsOverviewService {
) { } ) { }
async getTimesheetsForEmployeeByPeriod( async getTimesheetsForEmployeeByPeriod(
email: string, email: string,
pay_year: number, pay_year: number,
pay_period_no: number, pay_period_no: number,
employee_email?: string employee_email?: string
): Promise<Result<Timesheets, string>> { ): Promise<Result<Timesheets, string>> {
try { try {
@ -56,16 +55,28 @@ export class GetTimesheetsOverviewService {
//find user infos using the employee_id //find user infos using the employee_id
const employee = await this.prisma.employees.findUnique({ const employee = await this.prisma.employees.findUnique({
where: { id: employee_id.data }, where: { id: employee_id.data },
select: { daily_expected_hours: true, schedule_preset: true, user: true }, select: { schedule_preset: true, user: true, id: true }
}); });
if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` } if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` };
if (!employee.user) return { success: false, error: 'USER_NOT_FOUND' };
const contractDetails = await this.prisma.contracts.findUnique({
where: { employee_id: employee.id },
select: {
daily_expected_hours: true,
applicable_overtime: true,
phone_allocation: true,
on_call_allocation: true,
weekend_on_call_allocation: true,
}
});
if (!contractDetails) return { success: false, error: 'CONTRACT_NOT_FOUND' };
//builds employee details //builds employee details
const has_preset_schedule = employee.schedule_preset !== null; const has_preset_schedule = employee.schedule_preset !== null;
const user = employee.user; const user = employee.user;
const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
//maps all timesheet's infos //maps all timesheet's infos
const timesheets = await Promise.all(rows.map((timesheet) => mapOneTimesheet(timesheet))); const timesheets = await Promise.all(rows.map((timesheet) => mapOneTimesheet(timesheet)));
if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' } if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' }
@ -73,7 +84,13 @@ export class GetTimesheetsOverviewService {
const data: Timesheets = { const data: Timesheets = {
has_preset_schedule, has_preset_schedule,
employee_fullname, employee_fullname,
daily_expected_hours: employee.daily_expected_hours, contract: {
daily_expected_hours: contractDetails.daily_expected_hours,
applicable_overtime: contractDetails.applicable_overtime,
phone_allocation: Number(contractDetails.phone_allocation),
on_call_allocation: Number(contractDetails.on_call_allocation),
weekend_on_call_allocation: Number(contractDetails.weekend_on_call_allocation),
},
timesheets, timesheets,
} }
@ -85,8 +102,8 @@ export class GetTimesheetsOverviewService {
} }
private async loadTimesheets( private async loadTimesheets(
employee_id: number, employee_id: number,
period_start: Date, period_start: Date,
period_end: Date period_end: Date
) { ) {
return this.prisma.timesheets.findMany({ return this.prisma.timesheets.findMany({
@ -101,7 +118,7 @@ export class GetTimesheetsOverviewService {
} }
private ensureTimesheet = async ( private ensureTimesheet = async (
employee_id: number, employee_id: number,
start_date: Date | string start_date: Date | string
) => { ) => {
const start = toDateFromString(start_date); const start = toDateFromString(start_date);

View File

@ -1,5 +1,5 @@
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator"; import { IsArray, IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator";
export class TimesheetEntity { export class TimesheetEntity {
@IsInt() id: number; @IsInt() id: number;
@ -11,10 +11,19 @@ export class TimesheetEntity {
export class Timesheets { export class Timesheets {
@IsBoolean() has_preset_schedule: boolean; @IsBoolean() has_preset_schedule: boolean;
@IsString() employee_fullname: string; @IsString() employee_fullname: string;
@IsInt() daily_expected_hours: number; @Type(() => Contract) contract: Contract;
@Type(() => Number)
@Type(() => Timesheet) timesheets: Timesheet[]; @Type(() => Timesheet) timesheets: Timesheet[];
} }
export class Contract {
@Type(() => Number) daily_expected_hours: number;
@Type(() => Number) phone_allocation: number;
@Type(() => Number) on_call_allocation: number;
@Type(() => Number) weekend_on_call_allocation: number;
@IsArray() @IsString() applicable_overtime: string[];
}
export class Timesheet { export class Timesheet {
@IsInt() timesheet_id: number; @IsInt() timesheet_id: number;
@IsBoolean() is_approved: boolean; @IsBoolean() is_approved: boolean;